diff options
author | 2023-08-20 13:35:55 +0200 | |
---|---|---|
committer | 2023-08-20 13:35:55 +0200 | |
commit | 1e2db7a32f72ee01497a08c67e6f7f507890ee71 (patch) | |
tree | 76a6e64c3897ff183383bdb20b185f42cc462a16 /internal/middleware | |
parent | [feature] Instance rules (#2125) (diff) | |
download | gotosocial-1e2db7a32f72ee01497a08c67e6f7f507890ee71.tar.xz |
[feature/bugfix] Probe S3 storage for CSP uri, add config flag for extra URIs (#2134)
* [feature/bugfix] Probe S3 storage for CSP uri, add config flag for extra URIs
* env parsing tests, my coy mistress
Diffstat (limited to 'internal/middleware')
-rw-r--r-- | internal/middleware/contentsecuritypolicy.go | 144 | ||||
-rw-r--r-- | internal/middleware/extraheaders.go | 55 | ||||
-rw-r--r-- | internal/middleware/middleware_test.go | 79 |
3 files changed, 170 insertions, 108 deletions
diff --git a/internal/middleware/contentsecuritypolicy.go b/internal/middleware/contentsecuritypolicy.go new file mode 100644 index 000000000..5984a75c3 --- /dev/null +++ b/internal/middleware/contentsecuritypolicy.go @@ -0,0 +1,144 @@ +// 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 <http://www.gnu.org/licenses/>. + +package middleware + +import ( + "strings" + + "codeberg.org/gruf/go-debug" + "github.com/gin-gonic/gin" +) + +func ContentSecurityPolicy(extraURIs ...string) gin.HandlerFunc { + csp := BuildContentSecurityPolicy(extraURIs...) + + return func(c *gin.Context) { + // Inform the browser we only load + // CSS/JS/media using the given policy. + c.Header("Content-Security-Policy", csp) + } +} + +func BuildContentSecurityPolicy(extraURIs ...string) string { + const ( + defaultSrc = "default-src" + objectSrc = "object-src" + imgSrc = "img-src" + mediaSrc = "media-src" + + self = "'self'" + none = "'none'" + blob = "blob:" + ) + + // CSP values keyed by directive. + values := make(map[string][]string, 4) + + /* + default-src + https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src + */ + + if !debug.DEBUG { + // Restrictive 'self' policy + values[defaultSrc] = []string{self} + } else { + // If debug is enabled, allow + // serving things from localhost + // as well (regardless of port). + values[defaultSrc] = []string{ + self, + "localhost:*", + "ws://localhost:*", + } + } + + /* + object-src + https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/object-src + */ + + // Disallow object-src as recommended. + values[objectSrc] = []string{none} + + /* + img-src + https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/img-src + */ + + // Restrictive 'self' policy, + // include extraURIs, and 'blob:' + // for previewing uploaded images + // (header, avi, emojis) in settings. + values[imgSrc] = append( + []string{self, blob}, + extraURIs..., + ) + + /* + media-src + https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/media-src + */ + + // Restrictive 'self' policy, + // include extraURIs. + values[mediaSrc] = append( + []string{self}, + extraURIs..., + ) + + /* + Assemble policy directives. + */ + + // Iterate through an ordered slice rather than + // iterating through the map, since we want these + // policyDirectives in a determinate order. + policyDirectives := make([]string, 4) + for i, directive := range []string{ + defaultSrc, + objectSrc, + imgSrc, + mediaSrc, + } { + // Each policy directive should look like: + // `[directive] [value1] [value2] [etc]` + + // Get assembled values + // for this directive. + values := values[directive] + + // Prepend values with + // the directive name. + directiveValues := append( + []string{directive}, + values..., + ) + + // Space-separate them. + policyDirective := strings.Join(directiveValues, " ") + + // Done. + policyDirectives[i] = policyDirective + } + + // Content-security-policy looks like this: + // `Content-Security-Policy: <policy-directive>; <policy-directive>` + // So join each policy directive appropriately. + return strings.Join(policyDirectives, "; ") +} diff --git a/internal/middleware/extraheaders.go b/internal/middleware/extraheaders.go index 064e85cca..1a3f1d522 100644 --- a/internal/middleware/extraheaders.go +++ b/internal/middleware/extraheaders.go @@ -18,15 +18,11 @@ package middleware 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 { - csp := BuildContentSecurityPolicy() - return func(c *gin.Context) { // Inform all callers which server implementation this is. c.Header("Server", "gotosocial") @@ -39,56 +35,5 @@ 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 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:* ws://localhost:*" - } - - // Disallow object-src as recommended https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/object-src - policy += "; object-src 'none'" - - s3Endpoint := config.GetStorageS3Endpoint() - if s3Endpoint == "" || config.GetStorageS3Proxy() { - // S3 not configured or in proxy mode, just allow images from self and blob: - policy += "; img-src 'self' blob:" - 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 - - // When object storage is in use in non-proxied mode, GtS still serves some - // assets itself like the logo, so keep 'self' in there. That should also - // handle any redirects from the fileserver to object storage. - - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/img-src - policy += "; img-src 'self' blob: " + s3EndpointURLStr - - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/media-src - policy += "; media-src 'self' " + s3EndpointURLStr - - return policy } diff --git a/internal/middleware/middleware_test.go b/internal/middleware/middleware_test.go index 29376304e..fad05931b 100644 --- a/internal/middleware/middleware_test.go +++ b/internal/middleware/middleware_test.go @@ -20,80 +20,53 @@ 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 + extraURLs []string + expected string } for _, test := range []cspTest{ { - s3Endpoint: "", - s3Proxy: false, - s3Secure: false, - expected: "default-src 'self'; object-src 'none'; img-src 'self' blob:", + extraURLs: nil, + expected: "default-src 'self'; object-src 'none'; img-src 'self' blob:; media-src 'self'", }, { - s3Endpoint: "some-bucket-provider.com", - s3Proxy: false, - s3Secure: true, - expected: "default-src 'self'; object-src 'none'; img-src 'self' blob: https://some-bucket-provider.com; media-src 'self' https://some-bucket-provider.com", + extraURLs: []string{ + "https://some-bucket-provider.com", + }, + expected: "default-src 'self'; object-src 'none'; img-src 'self' blob: https://some-bucket-provider.com; media-src 'self' https://some-bucket-provider.com", }, { - s3Endpoint: "some-bucket-provider.com:6969", - s3Proxy: false, - s3Secure: true, - expected: "default-src 'self'; object-src 'none'; img-src 'self' blob: https://some-bucket-provider.com:6969; media-src 'self' https://some-bucket-provider.com:6969", + extraURLs: []string{ + "https://some-bucket-provider.com:6969", + }, + expected: "default-src 'self'; object-src 'none'; img-src 'self' blob: https://some-bucket-provider.com:6969; media-src 'self' https://some-bucket-provider.com:6969", }, { - s3Endpoint: "some-bucket-provider.com:6969", - s3Proxy: false, - s3Secure: false, - expected: "default-src 'self'; object-src 'none'; img-src 'self' blob: http://some-bucket-provider.com:6969; media-src 'self' http://some-bucket-provider.com:6969", + extraURLs: []string{ + "http://some-bucket-provider.com:6969", + }, + expected: "default-src 'self'; object-src 'none'; img-src 'self' blob: http://some-bucket-provider.com:6969; media-src 'self' http://some-bucket-provider.com:6969", }, { - s3Endpoint: "s3.nl-ams.scw.cloud", - s3Proxy: false, - s3Secure: true, - expected: "default-src 'self'; object-src 'none'; img-src 'self' blob: https://s3.nl-ams.scw.cloud; media-src 'self' https://s3.nl-ams.scw.cloud", + extraURLs: []string{ + "https://s3.nl-ams.scw.cloud", + }, + expected: "default-src 'self'; object-src 'none'; img-src 'self' blob: https://s3.nl-ams.scw.cloud; media-src 'self' https://s3.nl-ams.scw.cloud", }, { - s3Endpoint: "some-bucket-provider.com", - s3Proxy: true, - s3Secure: true, - expected: "default-src 'self'; object-src 'none'; img-src 'self' blob:", - }, - { - s3Endpoint: "some-bucket-provider.com:6969", - s3Proxy: true, - s3Secure: true, - expected: "default-src 'self'; object-src 'none'; img-src 'self' blob:", - }, - { - s3Endpoint: "some-bucket-provider.com:6969", - s3Proxy: true, - s3Secure: true, - expected: "default-src 'self'; object-src 'none'; img-src 'self' blob:", - }, - { - s3Endpoint: "s3.nl-ams.scw.cloud", - s3Proxy: true, - s3Secure: true, - expected: "default-src 'self'; object-src 'none'; img-src 'self' blob:", + extraURLs: []string{ + "https://s3.nl-ams.scw.cloud", + "https://s3.somewhere.else.example.org", + }, + expected: "default-src 'self'; object-src 'none'; img-src 'self' blob: https://s3.nl-ams.scw.cloud https://s3.somewhere.else.example.org; media-src 'self' https://s3.nl-ams.scw.cloud https://s3.somewhere.else.example.org", }, } { - config.SetStorageS3Endpoint(test.s3Endpoint) - config.SetStorageS3Proxy(test.s3Proxy) - config.SetStorageS3UseSSL(test.s3Secure) - - csp := middleware.BuildContentSecurityPolicy() + csp := middleware.BuildContentSecurityPolicy(test.extraURLs...) if csp != test.expected { t.Logf("expected '%s', got '%s'", test.expected, csp) t.Fail() |