summaryrefslogtreecommitdiff
path: root/internal/middleware
diff options
context:
space:
mode:
Diffstat (limited to 'internal/middleware')
-rw-r--r--internal/middleware/contentsecuritypolicy.go144
-rw-r--r--internal/middleware/extraheaders.go55
-rw-r--r--internal/middleware/middleware_test.go79
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()