From 1197b1dd8de87ce2231b1c2c58407c78e73b0f81 Mon Sep 17 00:00:00 2001 From: LP B Date: Wed, 13 Aug 2025 22:07:55 +0200 Subject: [PATCH] feat(api): Permissions-Policy header deny all (#1021) --- api/http/handler/file/handler.go | 4 + api/http/handler/file/handler_test.go | 70 +++++++++++++++++ api/http/handler/file/permissions_list.go | 91 +++++++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 api/http/handler/file/handler_test.go create mode 100644 api/http/handler/file/permissions_list.go diff --git a/api/http/handler/file/handler.go b/api/http/handler/file/handler.go index 9e57478c8..f0e4435e5 100644 --- a/api/http/handler/file/handler.go +++ b/api/http/handler/file/handler.go @@ -55,6 +55,10 @@ func (handler *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } + if r.RequestURI == "/" || strings.HasSuffix(r.RequestURI, ".html") { + w.Header().Set("Permissions-Policy", strings.Join(permissions, ",")) + } + if !isHTML(r.Header["Accept"]) { w.Header().Set("Cache-Control", "max-age=31536000") } else { diff --git a/api/http/handler/file/handler_test.go b/api/http/handler/file/handler_test.go new file mode 100644 index 000000000..411272882 --- /dev/null +++ b/api/http/handler/file/handler_test.go @@ -0,0 +1,70 @@ +package file_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/portainer/portainer/api/http/handler/file" + "github.com/stretchr/testify/require" +) + +func TestNormalServe(t *testing.T) { + handler := file.NewHandler("", false, func() bool { return false }) + require.NotNil(t, handler) + + request := func(path string) (*http.Request, *httptest.ResponseRecorder) { + rr := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, path, nil) + handler.ServeHTTP(rr, req) + return req, rr + } + + _, rr := request("/timeout.html") + require.Equal(t, http.StatusTemporaryRedirect, rr.Result().StatusCode) + loc, err := rr.Result().Location() + require.NoError(t, err) + require.NotNil(t, loc) + require.Equal(t, "/", loc.Path) + + _, rr = request("/") + require.Equal(t, http.StatusOK, rr.Result().StatusCode) +} + +func TestPermissionsPolicyHeader(t *testing.T) { + handler := file.NewHandler("", false, func() bool { return false }) + require.NotNil(t, handler) + + test := func(path string, exist bool) { + rr := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, path, nil) + handler.ServeHTTP(rr, req) + + require.Equal(t, exist, rr.Result().Header.Get("Permissions-Policy") != "") + } + + test("/", true) + test("/index.html", true) + test("/api", false) + test("/an/image.png", false) +} + +func TestRedirectInstanceDisabled(t *testing.T) { + handler := file.NewHandler("", false, func() bool { return true }) + require.NotNil(t, handler) + + test := func(path string) { + rr := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, path, nil) + handler.ServeHTTP(rr, req) + + require.Equal(t, http.StatusTemporaryRedirect, rr.Result().StatusCode) + loc, err := rr.Result().Location() + require.NoError(t, err) + require.NotNil(t, loc) + require.Equal(t, "/timeout.html", loc.Path) + } + + test("/") + test("/index.html") +} diff --git a/api/http/handler/file/permissions_list.go b/api/http/handler/file/permissions_list.go new file mode 100644 index 000000000..4355372d9 --- /dev/null +++ b/api/http/handler/file/permissions_list.go @@ -0,0 +1,91 @@ +package file + +var permissions = []string{ + "accelerometer=()", + "ambient-light-sensor=()", + "attribution-reporting=()", + "autoplay=()", + "battery=()", + "browsing-topics=()", + "camera=()", + "captured-surface-control=()", + "ch-device-memory=()", + "ch-downlink=()", + "ch-dpr=()", + "ch-ect=()", + "ch-prefers-color-scheme=()", + "ch-prefers-reduced-motion=()", + "ch-prefers-reduced-transparency=()", + "ch-rtt=()", + "ch-save-data=()", + "ch-ua=()", + "ch-ua-arch=()", + "ch-ua-bitness=()", + "ch-ua-form-factors=()", + "ch-ua-full-version=()", + "ch-ua-full-version-list=()", + "ch-ua-mobile=()", + "ch-ua-model=()", + "ch-ua-platform=()", + "ch-ua-platform-version=()", + "ch-ua-wow64=()", + "ch-viewport-height=()", + "ch-viewport-width=()", + "ch-width=()", + "compute-pressure=()", + "conversion-measurement=()", + "cross-origin-isolated=()", + "deferred-fetch=()", + "deferred-fetch-minimal=()", + "display-capture=()", + "document-domain=()", + "encrypted-media=()", + "execution-while-not-rendered=()", + "execution-while-out-of-viewport=()", + "focus-without-user-activation=()", + "fullscreen=()", + "gamepad=()", + "geolocation=()", + "gyroscope=()", + "hid=()", + "identity-credentials-get=()", + "idle-detection=()", + "interest-cohort=()", + "join-ad-interest-group=()", + "keyboard-map=()", + "language-detector=()", + "local-fonts=()", + "magnetometer=()", + "microphone=()", + "midi=()", + "navigation-override=()", + "otp-credentials=()", + "payment=()", + "picture-in-picture=()", + "private-aggregation=()", + "private-state-token-issuance=()", + "private-state-token-redemption=()", + "publickey-credentials-create=()", + "publickey-credentials-get=()", + "rewriter=()", + "run-ad-auction=()", + "screen-wake-lock=()", + "serial=()", + "shared-storage=()", + "shared-storage-select-url=()", + "speaker-selection=()", + "storage-access=()", + "summarizer=()", + "sync-script=()", + "sync-xhr=()", + "translator=()", + "trust-token-redemption=()", + "unload=()", + "usb=()", + "vertical-scroll=()", + "web-share=()", + "window-management=()", + "window-placement=()", + "writer=()", + "xr-spatial-tracking=()", +}