Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature/bugfix] Probe S3 storage for CSP uri, add config flag for extra URIs #2134

Merged
merged 2 commits into from Aug 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 23 additions & 0 deletions cmd/gotosocial/action/server/server.go
Expand Up @@ -204,6 +204,29 @@ var Start action.GTSAction = func(ctx context.Context) error {
middleware.ExtraHeaders(),
}...)

// Instantiate Content-Security-Policy
// middleware, with extra URIs.
cspExtraURIs := make([]string, 0)

// Probe storage to check if extra URI is needed in CSP.
// Error here means something is wrong with storage.
storageCSPUri, err := state.Storage.ProbeCSPUri(ctx)
if err != nil {
return fmt.Errorf("error deriving Content-Security-Policy uri from storage: %w", err)
}

// storageCSPUri may be empty string if
// not S3-backed storage; check for this.
if storageCSPUri != "" {
cspExtraURIs = append(cspExtraURIs, storageCSPUri)
}

// Add any extra CSP URIs from config.
cspExtraURIs = append(cspExtraURIs, config.GetAdvancedCSPExtraURIs()...)

// Add CSP to middlewares.
middlewares = append(middlewares, middleware.ContentSecurityPolicy(cspExtraURIs...))

// attach global middlewares which are used for every request
router.AttachGlobalMiddleware(middlewares...)

Expand Down
30 changes: 28 additions & 2 deletions cmd/gotosocial/action/testrig/testrig.go
Expand Up @@ -70,7 +70,11 @@ var Start action.GTSAction = func(ctx context.Context) error {
testrig.StandardDBSetup(state.DB, nil)

if os.Getenv("GTS_STORAGE_BACKEND") == "s3" {
state.Storage, _ = storage.NewS3Storage()
var err error
state.Storage, err = storage.NewS3Storage()
if err != nil {
return fmt.Errorf("error initializing storage: %w", err)
}
} else {
state.Storage = testrig.NewInMemoryStorage()
}
Expand Down Expand Up @@ -136,6 +140,29 @@ var Start action.GTSAction = func(ctx context.Context) error {
middleware.ExtraHeaders(),
}...)

// Instantiate Content-Security-Policy
// middleware, with extra URIs.
cspExtraURIs := make([]string, 0)

// Probe storage to check if extra URI is needed in CSP.
// Error here means something is wrong with storage.
storageCSPUri, err := state.Storage.ProbeCSPUri(ctx)
if err != nil {
return fmt.Errorf("error deriving Content-Security-Policy uri from storage: %w", err)
}

// storageCSPUri may be empty string if
// not S3-backed storage; check for this.
if storageCSPUri != "" {
cspExtraURIs = append(cspExtraURIs, storageCSPUri)
}

// Add any extra CSP URIs from config.
cspExtraURIs = append(cspExtraURIs, config.GetAdvancedCSPExtraURIs()...)

// Add CSP to middlewares.
middlewares = append(middlewares, middleware.ContentSecurityPolicy(cspExtraURIs...))

// attach global middlewares which are used for every request
router.AttachGlobalMiddleware(middlewares...)

Expand All @@ -146,7 +173,6 @@ var Start action.GTSAction = func(ctx context.Context) error {

// build router modules
var idp oidc.IDP
var err error
if config.GetOIDCEnabled() {
idp, err = oidc.NewIDP(ctx)
if err != nil {
Expand Down
18 changes: 18 additions & 0 deletions docs/configuration/advanced.md
Expand Up @@ -118,4 +118,22 @@ advanced-throttling-retry-after: "30s"
# 2 cpu = 1 concurrent sender
# 4 cpu = 1 concurrent sender
advanced-sender-multiplier: 2

# Array of string. Extra URIs to add to 'img-src' and 'media-src'
# when building the Content-Security-Policy header for your instance.
#
# This can be used to allow the browser to load resources from additional
# sources like S3 buckets and so on when viewing your instance's pages
# and profiles in the browser.
#
# Since non-proxying S3 storage will be probed on instance launch to
# generate a correct Content-Security-Policy, you probably won't need
# to ever touch this setting, but it's included in the 'spirit of more
# configurable (usually) means more good'.
#
# See: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
#
# Example: ["s3.example.org", "some-bucket-name.s3.example.org"]
# Default: []
advanced-csp-extra-uris: []
```
18 changes: 18 additions & 0 deletions example/config.yaml
Expand Up @@ -903,3 +903,21 @@ advanced-throttling-retry-after: "30s"
# 2 cpu = 1 concurrent sender
# 4 cpu = 1 concurrent sender
advanced-sender-multiplier: 2

# Array of string. Extra URIs to add to 'img-src' and 'media-src'
# when building the Content-Security-Policy header for your instance.
#
# This can be used to allow the browser to load resources from additional
# sources like S3 buckets and so on when viewing your instance's pages
# and profiles in the browser.
#
# Since non-proxying S3 storage will be probed on instance launch to
# generate a correct Content-Security-Policy, you probably won't need
# to ever touch this setting, but it's included in the 'spirit of more
# configurable (usually) means more good'.
#
# See: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
#
# Example: ["s3.example.org", "some-bucket-name.s3.example.org"]
# Default: []
advanced-csp-extra-uris: []
1 change: 1 addition & 0 deletions internal/config/config.go
Expand Up @@ -150,6 +150,7 @@ type Configuration struct {
AdvancedThrottlingMultiplier int `name:"advanced-throttling-multiplier" usage:"Multiplier to use per cpu for http request throttling. 0 or less turns throttling off."`
AdvancedThrottlingRetryAfter time.Duration `name:"advanced-throttling-retry-after" usage:"Retry-After duration response to send for throttled requests."`
AdvancedSenderMultiplier int `name:"advanced-sender-multiplier" usage:"Multiplier to use per cpu for batching outgoing fedi messages. 0 or less turns batching off (not recommended)."`
AdvancedCSPExtraURIs []string `name:"advanced-csp-extra-uris" usage:"Additional URIs to allow when building content-security-policy for media + images."`

// HTTPClient configuration vars.
HTTPClient HTTPClientConfiguration `name:"http-client"`
Expand Down
1 change: 1 addition & 0 deletions internal/config/defaults.go
Expand Up @@ -124,6 +124,7 @@ var Defaults = Configuration{
AdvancedThrottlingMultiplier: 8, // 8 open requests per CPU
AdvancedThrottlingRetryAfter: time.Second * 30,
AdvancedSenderMultiplier: 2, // 2 senders per CPU
AdvancedCSPExtraURIs: []string{},

Cache: CacheConfiguration{
// Rough memory target that the total
Expand Down
1 change: 1 addition & 0 deletions internal/config/flags.go
Expand Up @@ -151,6 +151,7 @@ func (s *ConfigState) AddServerFlags(cmd *cobra.Command) {
cmd.Flags().Int(AdvancedThrottlingMultiplierFlag(), cfg.AdvancedThrottlingMultiplier, fieldtag("AdvancedThrottlingMultiplier", "usage"))
cmd.Flags().Duration(AdvancedThrottlingRetryAfterFlag(), cfg.AdvancedThrottlingRetryAfter, fieldtag("AdvancedThrottlingRetryAfter", "usage"))
cmd.Flags().Int(AdvancedSenderMultiplierFlag(), cfg.AdvancedSenderMultiplier, fieldtag("AdvancedSenderMultiplier", "usage"))
cmd.Flags().StringSlice(AdvancedCSPExtraURIsFlag(), cfg.AdvancedCSPExtraURIs, fieldtag("AdvancedCSPExtraURIs", "usage"))

cmd.Flags().String(RequestIDHeaderFlag(), cfg.RequestIDHeader, fieldtag("RequestIDHeader", "usage"))
})
Expand Down
25 changes: 25 additions & 0 deletions internal/config/helpers.gen.go
Expand Up @@ -2324,6 +2324,31 @@ func GetAdvancedSenderMultiplier() int { return global.GetAdvancedSenderMultipli
// SetAdvancedSenderMultiplier safely sets the value for global configuration 'AdvancedSenderMultiplier' field
func SetAdvancedSenderMultiplier(v int) { global.SetAdvancedSenderMultiplier(v) }

// GetAdvancedCSPExtraURIs safely fetches the Configuration value for state's 'AdvancedCSPExtraURIs' field
func (st *ConfigState) GetAdvancedCSPExtraURIs() (v []string) {
st.mutex.RLock()
v = st.config.AdvancedCSPExtraURIs
st.mutex.RUnlock()
return
}

// SetAdvancedCSPExtraURIs safely sets the Configuration value for state's 'AdvancedCSPExtraURIs' field
func (st *ConfigState) SetAdvancedCSPExtraURIs(v []string) {
st.mutex.Lock()
defer st.mutex.Unlock()
st.config.AdvancedCSPExtraURIs = v
st.reloadToViper()
}

// AdvancedCSPExtraURIsFlag returns the flag name for the 'AdvancedCSPExtraURIs' field
func AdvancedCSPExtraURIsFlag() string { return "advanced-csp-extra-uris" }

// GetAdvancedCSPExtraURIs safely fetches the value for global configuration 'AdvancedCSPExtraURIs' field
func GetAdvancedCSPExtraURIs() []string { return global.GetAdvancedCSPExtraURIs() }

// SetAdvancedCSPExtraURIs safely sets the value for global configuration 'AdvancedCSPExtraURIs' field
func SetAdvancedCSPExtraURIs(v []string) { global.SetAdvancedCSPExtraURIs(v) }

// GetHTTPClientAllowIPs safely fetches the Configuration value for state's 'HTTPClient.AllowIPs' field
func (st *ConfigState) GetHTTPClientAllowIPs() (v []string) {
st.mutex.RLock()
Expand Down
144 changes: 144 additions & 0 deletions 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, "; ")
}
55 changes: 0 additions & 55 deletions internal/middleware/extraheaders.go
Expand Up @@ -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")
Expand All @@ -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
}