diff --git a/internal/middleware/extraheaders.go b/internal/middleware/extraheaders.go index f584633fe..cd207a9f1 100644 --- a/internal/middleware/extraheaders.go +++ b/internal/middleware/extraheaders.go @@ -20,17 +20,17 @@ import ( "codeberg.org/gruf/go-debug" "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/config" ) // ExtraHeaders returns a new gin middleware which adds various extra headers to the response. func ExtraHeaders() gin.HandlerFunc { - policy := "default-src 'self'" - if debug.DEBUG { - policy += " localhost:*" - } + csp := BuildContentSecurityPolicy() + return func(c *gin.Context) { // Inform all callers which server implementation this is. c.Header("Server", "gotosocial") + // Prevent google chrome cohort tracking. Originally this was referred // to as FlocBlock. Floc was replaced by Topics in 2022 and the spec says // that interest-cohort will also block Topics (as of 2022-Nov). @@ -39,7 +39,55 @@ func ExtraHeaders() gin.HandlerFunc { // // See: https://github.com/patcg-individual-drafts/topics c.Header("Permissions-Policy", "browsing-topics=()") - // Inform the browser we only load CSS/JS/media from the same domain - c.Header("Content-Security-Policy", policy) + + // Inform the browser we only load + // CSS/JS/media using the given policy. + c.Header("Content-Security-Policy", csp) } } + +func BuildContentSecurityPolicy() string { + // Start with restrictive policy. + policy := "default-src 'self'" + + if debug.DEBUG { + // Debug is enabled, allow + // serving things from localhost + // as well (regardless of port). + policy += " localhost:*" + } + + s3Endpoint := config.GetStorageS3Endpoint() + if s3Endpoint == "" { + // S3 not configured, + // default policy is OK. + return policy + } + + if config.GetStorageS3Proxy() { + // S3 is configured in proxy + // mode, default policy is OK. + return policy + } + + // S3 is on and in non-proxy mode, so we need to add the S3 host to + // the policy to allow images and video to be pulled from there too. + + // If secure is false, + // use 'http' scheme. + scheme := "https" + if !config.GetStorageS3UseSSL() { + scheme = "http" + } + + // Construct endpoint URL. + s3EndpointURLStr := scheme + "://" + s3Endpoint + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/img-src + policy += "; image-src " + s3EndpointURLStr + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/media-src + policy += "; media-src " + s3EndpointURLStr + + return policy +} diff --git a/internal/middleware/middleware_test.go b/internal/middleware/middleware_test.go new file mode 100644 index 000000000..fecae5dd1 --- /dev/null +++ b/internal/middleware/middleware_test.go @@ -0,0 +1,102 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package middleware_test + +import ( + "testing" + + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/middleware" +) + +func TestBuildContentSecurityPolicy(t *testing.T) { + type cspTest struct { + s3Endpoint string + s3Proxy bool + s3Secure bool + expected string + actual string + } + + for _, test := range []cspTest{ + { + s3Endpoint: "", + s3Proxy: false, + s3Secure: false, + expected: "default-src 'self'", + }, + { + s3Endpoint: "some-bucket-provider.com", + s3Proxy: false, + s3Secure: true, + expected: "default-src 'self'; image-src https://some-bucket-provider.com; media-src https://some-bucket-provider.com", + }, + { + s3Endpoint: "some-bucket-provider.com:6969", + s3Proxy: false, + s3Secure: true, + expected: "default-src 'self'; image-src https://some-bucket-provider.com:6969; media-src https://some-bucket-provider.com:6969", + }, + { + s3Endpoint: "some-bucket-provider.com:6969", + s3Proxy: false, + s3Secure: false, + expected: "default-src 'self'; image-src http://some-bucket-provider.com:6969; media-src http://some-bucket-provider.com:6969", + }, + { + s3Endpoint: "s3.nl-ams.scw.cloud", + s3Proxy: false, + s3Secure: true, + expected: "default-src 'self'; image-src https://s3.nl-ams.scw.cloud; media-src https://s3.nl-ams.scw.cloud", + }, + { + s3Endpoint: "some-bucket-provider.com", + s3Proxy: true, + s3Secure: true, + expected: "default-src 'self'", + }, + { + s3Endpoint: "some-bucket-provider.com:6969", + s3Proxy: true, + s3Secure: true, + expected: "default-src 'self'", + }, + { + s3Endpoint: "some-bucket-provider.com:6969", + s3Proxy: true, + s3Secure: true, + expected: "default-src 'self'", + }, + { + s3Endpoint: "s3.nl-ams.scw.cloud", + s3Proxy: true, + s3Secure: true, + expected: "default-src 'self'", + }, + } { + config.SetStorageS3Endpoint(test.s3Endpoint) + config.SetStorageS3Proxy(test.s3Proxy) + config.SetStorageS3UseSSL(test.s3Secure) + + csp := middleware.BuildContentSecurityPolicy() + if csp != test.expected { + t.Logf("expected '%s', got '%s'", test.expected, csp) + t.Fail() + } + } +}