Thanks to visit codestin.com
Credit goes to github.com

Skip to content

chore: add deployment config option to append custom csp directives #15596

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

Merged
merged 6 commits into from
Nov 21, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cli/testdata/coder_server_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,13 @@ backed by Tailscale and WireGuard.
+ 1`. Use special value 'disable' to turn off STUN completely.

NETWORKING / HTTP OPTIONS:
--additional-csp-policy string-array, $CODER_ADDITIONAL_CSP_POLICY
Coder configures a Content Security Policy (CSP) to protect against
XSS attacks. This setting allows you to add additional CSP directives,
which can open the attack surface of the deployment. Format matches
the CSP directive format, e.g. --additional-csp-policy="script-src
https://example.com".

--disable-password-auth bool, $CODER_DISABLE_PASSWORD_AUTH
Disable password authentication. This is recommended for security
purposes in production deployments that rely on an identity provider.
Expand Down
6 changes: 6 additions & 0 deletions cli/testdata/server-config.yaml.golden
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ networking:
# HTTP bind address of the server. Unset to disable the HTTP endpoint.
# (default: 127.0.0.1:3000, type: string)
httpAddress: 127.0.0.1:3000
# Coder configures a Content Security Policy (CSP) to protect against XSS attacks.
# This setting allows you to add additional CSP directives, which can open the
# attack surface of the deployment. Format matches the CSP directive format, e.g.
# --additional-csp-policy="script-src https://example.com".
# (default: <unset>, type: string-array)
additionalCSPPolicy: []
# The maximum lifetime duration users can specify when creating an API token.
# (default: 876600h0m0s, type: duration)
maxTokenLifetime: 876600h0m0s
Expand Down
6 changes: 6 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 22 additions & 1 deletion coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto/tls"
"crypto/x509"
"database/sql"
"errors"
"expvar"
"flag"
"fmt"
Expand Down Expand Up @@ -1378,6 +1379,26 @@ func New(options *Options) *API {
r.Get("/swagger/*", swaggerDisabled)
}

additionalCSPHeaders := make(map[httpmw.CSPFetchDirective][]string, len(api.DeploymentValues.AdditionalCSPPolicy))
var cspParseErrors error
for _, v := range api.DeploymentValues.AdditionalCSPPolicy {
// Format is "<directive> <value> <value> ..."
v = strings.TrimSpace(v)
parts := strings.Split(v, " ")
if len(parts) < 2 {
cspParseErrors = errors.Join(cspParseErrors, xerrors.Errorf("invalid CSP header %q, not enough parts to be valid", v))
continue
}
additionalCSPHeaders[httpmw.CSPFetchDirective(strings.ToLower(parts[0]))] = parts[1:]
}

if cspParseErrors != nil {
// Do not fail Coder deployment startup because of this. Just log an error
// and continue
api.Logger.Error(context.Background(),
"parsing additional CSP headers", slog.Error(cspParseErrors))
}

// Add CSP headers to all static assets and pages. CSP headers only affect
// browsers, so these don't make sense on api routes.
cspMW := httpmw.CSPHeaders(options.Telemetry.Enabled(), func() []string {
Expand All @@ -1390,7 +1411,7 @@ func New(options *Options) *API {
}
// By default we do not add extra websocket connections to the CSP
return []string{}
})
}, additionalCSPHeaders)

// Static file handler must be wrapped with HSTS handler if the
// StrictTransportSecurityAge is set. We only need to set this header on
Expand Down
90 changes: 52 additions & 38 deletions coderd/httpmw/csp.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,29 +23,39 @@ func (s cspDirectives) Append(d CSPFetchDirective, values ...string) {
type CSPFetchDirective string

const (
cspDirectiveDefaultSrc = "default-src"
cspDirectiveConnectSrc = "connect-src"
cspDirectiveChildSrc = "child-src"
cspDirectiveScriptSrc = "script-src"
cspDirectiveFontSrc = "font-src"
cspDirectiveStyleSrc = "style-src"
cspDirectiveObjectSrc = "object-src"
cspDirectiveManifestSrc = "manifest-src"
cspDirectiveFrameSrc = "frame-src"
cspDirectiveImgSrc = "img-src"
cspDirectiveReportURI = "report-uri"
cspDirectiveFormAction = "form-action"
cspDirectiveMediaSrc = "media-src"
cspFrameAncestors = "frame-ancestors"
cspDirectiveWorkerSrc = "worker-src"
CSPDirectiveDefaultSrc CSPFetchDirective = "default-src"
CSPDirectiveConnectSrc CSPFetchDirective = "connect-src"
CSPDirectiveChildSrc CSPFetchDirective = "child-src"
CSPDirectiveScriptSrc CSPFetchDirective = "script-src"
CSPDirectiveFontSrc CSPFetchDirective = "font-src"
CSPDirectiveStyleSrc CSPFetchDirective = "style-src"
CSPDirectiveObjectSrc CSPFetchDirective = "object-src"
CSPDirectiveManifestSrc CSPFetchDirective = "manifest-src"
CSPDirectiveFrameSrc CSPFetchDirective = "frame-src"
CSPDirectiveImgSrc CSPFetchDirective = "img-src"
CSPDirectiveReportURI CSPFetchDirective = "report-uri"
CSPDirectiveFormAction CSPFetchDirective = "form-action"
CSPDirectiveMediaSrc CSPFetchDirective = "media-src"
CSPFrameAncestors CSPFetchDirective = "frame-ancestors"
CSPDirectiveWorkerSrc CSPFetchDirective = "worker-src"
)

// CSPHeaders returns a middleware that sets the Content-Security-Policy header
// for coderd. It takes a function that allows adding supported external websocket
// hosts. This is primarily to support the terminal connecting to a workspace proxy.
// for coderd.
//
// Arguments:
// - websocketHosts: a function that returns a list of supported external websocket hosts.
// This is to support the terminal connecting to a workspace proxy.
// The origin of the terminal request does not match the url of the proxy,
// so the CSP list of allowed hosts must be dynamic and match the current
// available proxy urls.
// - staticAdditions: a map of CSP directives to append to the default CSP headers.
// Used to allow specific static additions to the CSP headers. Allows some niche
// use cases, such as embedding Coder in an iframe.
// Example: https://github.com/coder/coder/issues/15118
//
//nolint:revive
func CSPHeaders(telemetry bool, websocketHosts func() []string) func(next http.Handler) http.Handler {
func CSPHeaders(telemetry bool, websocketHosts func() []string, staticAdditions map[CSPFetchDirective][]string) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Content-Security-Policy disables loading certain content types and can prevent XSS injections.
Expand All @@ -55,30 +65,30 @@ func CSPHeaders(telemetry bool, websocketHosts func() []string) func(next http.H
// The list of CSP options: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src
cspSrcs := cspDirectives{
// All omitted fetch csp srcs default to this.
cspDirectiveDefaultSrc: {"'self'"},
cspDirectiveConnectSrc: {"'self'"},
cspDirectiveChildSrc: {"'self'"},
CSPDirectiveDefaultSrc: {"'self'"},
CSPDirectiveConnectSrc: {"'self'"},
CSPDirectiveChildSrc: {"'self'"},
// https://github.com/suren-atoyan/monaco-react/issues/168
cspDirectiveScriptSrc: {"'self'"},
cspDirectiveStyleSrc: {"'self' 'unsafe-inline'"},
CSPDirectiveScriptSrc: {"'self'"},
CSPDirectiveStyleSrc: {"'self' 'unsafe-inline'"},
// data: is used by monaco editor on FE for Syntax Highlight
cspDirectiveFontSrc: {"'self' data:"},
cspDirectiveWorkerSrc: {"'self' blob:"},
CSPDirectiveFontSrc: {"'self' data:"},
CSPDirectiveWorkerSrc: {"'self' blob:"},
// object-src is needed to support code-server
cspDirectiveObjectSrc: {"'self'"},
CSPDirectiveObjectSrc: {"'self'"},
// blob: for loading the pwa manifest for code-server
cspDirectiveManifestSrc: {"'self' blob:"},
cspDirectiveFrameSrc: {"'self'"},
CSPDirectiveManifestSrc: {"'self' blob:"},
CSPDirectiveFrameSrc: {"'self'"},
// data: for loading base64 encoded icons for generic applications.
// https: allows loading images from external sources. This is not ideal
// but is required for the templates page that renders readmes.
// We should find a better solution in the future.
cspDirectiveImgSrc: {"'self' https: data:"},
cspDirectiveFormAction: {"'self'"},
cspDirectiveMediaSrc: {"'self'"},
CSPDirectiveImgSrc: {"'self' https: data:"},
CSPDirectiveFormAction: {"'self'"},
CSPDirectiveMediaSrc: {"'self'"},
// Report all violations back to the server to log
cspDirectiveReportURI: {"/api/v2/csp/reports"},
cspFrameAncestors: {"'none'"},
CSPDirectiveReportURI: {"/api/v2/csp/reports"},
CSPFrameAncestors: {"'none'"},

// Only scripts can manipulate the dom. This prevents someone from
// naming themselves something like '<svg onload="alert(/cross-site-scripting/)" />'.
Expand All @@ -87,7 +97,7 @@ func CSPHeaders(telemetry bool, websocketHosts func() []string) func(next http.H

if telemetry {
// If telemetry is enabled, we report to coder.com.
cspSrcs.Append(cspDirectiveConnectSrc, "https://coder.com")
cspSrcs.Append(CSPDirectiveConnectSrc, "https://coder.com")
}

// This extra connect-src addition is required to support old webkit
Expand All @@ -102,7 +112,7 @@ func CSPHeaders(telemetry bool, websocketHosts func() []string) func(next http.H
// We can add both ws:// and wss:// as browsers do not let https
// pages to connect to non-tls websocket connections. So this
// supports both http & https webpages.
cspSrcs.Append(cspDirectiveConnectSrc, fmt.Sprintf("wss://%[1]s ws://%[1]s", host))
cspSrcs.Append(CSPDirectiveConnectSrc, fmt.Sprintf("wss://%[1]s ws://%[1]s", host))
}

// The terminal requires a websocket connection to the workspace proxy.
Expand All @@ -112,15 +122,19 @@ func CSPHeaders(telemetry bool, websocketHosts func() []string) func(next http.H
for _, extraHost := range extraConnect {
if extraHost == "*" {
// '*' means all
cspSrcs.Append(cspDirectiveConnectSrc, "*")
cspSrcs.Append(CSPDirectiveConnectSrc, "*")
continue
}
cspSrcs.Append(cspDirectiveConnectSrc, fmt.Sprintf("wss://%[1]s ws://%[1]s", extraHost))
cspSrcs.Append(CSPDirectiveConnectSrc, fmt.Sprintf("wss://%[1]s ws://%[1]s", extraHost))
// We also require this to make http/https requests to the workspace proxy for latency checking.
cspSrcs.Append(cspDirectiveConnectSrc, fmt.Sprintf("https://%[1]s http://%[1]s", extraHost))
cspSrcs.Append(CSPDirectiveConnectSrc, fmt.Sprintf("https://%[1]s http://%[1]s", extraHost))
}
}

for directive, values := range staticAdditions {
cspSrcs.Append(directive, values...)
}

var csp strings.Builder
for src, vals := range cspSrcs {
_, _ = fmt.Fprintf(&csp, "%s %s; ", src, strings.Join(vals, " "))
Expand Down
6 changes: 6 additions & 0 deletions coderd/httpmw/csp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@ func TestCSPConnect(t *testing.T) {
t.Parallel()

expected := []string{"example.com", "coder.com"}
expectedMedia := []string{"media.com", "media2.com"}

r := httptest.NewRequest(http.MethodGet, "/", nil)
rw := httptest.NewRecorder()

httpmw.CSPHeaders(false, func() []string {
return expected
}, map[httpmw.CSPFetchDirective][]string{
httpmw.CSPDirectiveMediaSrc: expectedMedia,
})(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusOK)
})).ServeHTTP(rw, r)
Expand All @@ -30,4 +33,7 @@ func TestCSPConnect(t *testing.T) {
require.Containsf(t, rw.Header().Get("Content-Security-Policy"), fmt.Sprintf("ws://%s", e), "Content-Security-Policy header should contain ws://%s", e)
require.Containsf(t, rw.Header().Get("Content-Security-Policy"), fmt.Sprintf("wss://%s", e), "Content-Security-Policy header should contain wss://%s", e)
}
for _, e := range expectedMedia {
require.Containsf(t, rw.Header().Get("Content-Security-Policy"), e, "Content-Security-Policy header should contain %s", e)
}
}
13 changes: 13 additions & 0 deletions codersdk/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ type DeploymentValues struct {
CLIUpgradeMessage serpent.String `json:"cli_upgrade_message,omitempty" typescript:",notnull"`
TermsOfServiceURL serpent.String `json:"terms_of_service_url,omitempty" typescript:",notnull"`
Notifications NotificationsConfig `json:"notifications,omitempty" typescript:",notnull"`
AdditionalCSPPolicy serpent.StringArray `json:"additional_csp_policy,omitempty" typescript:",notnull"`

Config serpent.YAMLConfigPath `json:"config,omitempty" typescript:",notnull"`
WriteConfig serpent.Bool `json:"write_config,omitempty" typescript:",notnull"`
Expand Down Expand Up @@ -2147,6 +2148,18 @@ when required by your organization's security policy.`,
Group: &deploymentGroupIntrospectionLogging,
YAML: "enableTerraformDebugMode",
},
{
Name: "Additional CSP Policy",
Description: "Coder configures a Content Security Policy (CSP) to protect against XSS attacks. " +
"This setting allows you to add additional CSP directives, which can open the attack surface of the deployment. " +
"Format matches the CSP directive format, e.g. --additional-csp-policy=\"script-src https://example.com\".",
Flag: "additional-csp-policy",
Env: "CODER_ADDITIONAL_CSP_POLICY",
YAML: "additionalCSPPolicy",
Value: &c.AdditionalCSPPolicy,
Group: &deploymentGroupNetworkingHTTP,
},

// ☢️ Dangerous settings
{
Name: "DANGEROUS: Allow all CORS requests",
Expand Down
1 change: 1 addition & 0 deletions docs/reference/api/general.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions docs/reference/api/schemas.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions docs/reference/cli/server.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions enterprise/cli/testdata/coder_server_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,13 @@ backed by Tailscale and WireGuard.
+ 1`. Use special value 'disable' to turn off STUN completely.

NETWORKING / HTTP OPTIONS:
--additional-csp-policy string-array, $CODER_ADDITIONAL_CSP_POLICY
Coder configures a Content Security Policy (CSP) to protect against
XSS attacks. This setting allows you to add additional CSP directives,
which can open the attack surface of the deployment. Format matches
the CSP directive format, e.g. --additional-csp-policy="script-src
https://example.com".

--disable-password-auth bool, $CODER_DISABLE_PASSWORD_AUTH
Disable password authentication. This is recommended for security
purposes in production deployments that rely on an identity provider.
Expand Down
1 change: 1 addition & 0 deletions site/src/api/typesGenerated.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading