Skip to content

Commit 5b7fa78

Browse files
authored
chore: add deployment config option to append custom csp directives (#15596)
Allows adding custom static CSP directives to Coder. Niche use case but makes this easier then creating a reverse proxy that has to replace the header. We want to preserve our directives, so having an append option is preferred to a "replace" option via a reverse proxy. Closes #15118
1 parent f38f746 commit 5b7fa78

File tree

13 files changed

+140
-39
lines changed

13 files changed

+140
-39
lines changed

cli/testdata/coder_server_--help.golden

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,13 @@ backed by Tailscale and WireGuard.
294294
+ 1`. Use special value 'disable' to turn off STUN completely.
295295

296296
NETWORKING / HTTP OPTIONS:
297+
--additional-csp-policy string-array, $CODER_ADDITIONAL_CSP_POLICY
298+
Coder configures a Content Security Policy (CSP) to protect against
299+
XSS attacks. This setting allows you to add additional CSP directives,
300+
which can open the attack surface of the deployment. Format matches
301+
the CSP directive format, e.g. --additional-csp-policy="script-src
302+
https://example.com".
303+
297304
--disable-password-auth bool, $CODER_DISABLE_PASSWORD_AUTH
298305
Disable password authentication. This is recommended for security
299306
purposes in production deployments that rely on an identity provider.

cli/testdata/server-config.yaml.golden

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ networking:
1616
# HTTP bind address of the server. Unset to disable the HTTP endpoint.
1717
# (default: 127.0.0.1:3000, type: string)
1818
httpAddress: 127.0.0.1:3000
19+
# Coder configures a Content Security Policy (CSP) to protect against XSS attacks.
20+
# This setting allows you to add additional CSP directives, which can open the
21+
# attack surface of the deployment. Format matches the CSP directive format, e.g.
22+
# --additional-csp-policy="script-src https://example.com".
23+
# (default: <unset>, type: string-array)
24+
additionalCSPPolicy: []
1925
# The maximum lifetime duration users can specify when creating an API token.
2026
# (default: 876600h0m0s, type: duration)
2127
maxTokenLifetime: 876600h0m0s

coderd/apidoc/docs.go

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/coderd.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"crypto/tls"
66
"crypto/x509"
77
"database/sql"
8+
"errors"
89
"expvar"
910
"flag"
1011
"fmt"
@@ -1378,6 +1379,26 @@ func New(options *Options) *API {
13781379
r.Get("/swagger/*", swaggerDisabled)
13791380
}
13801381

1382+
additionalCSPHeaders := make(map[httpmw.CSPFetchDirective][]string, len(api.DeploymentValues.AdditionalCSPPolicy))
1383+
var cspParseErrors error
1384+
for _, v := range api.DeploymentValues.AdditionalCSPPolicy {
1385+
// Format is "<directive> <value> <value> ..."
1386+
v = strings.TrimSpace(v)
1387+
parts := strings.Split(v, " ")
1388+
if len(parts) < 2 {
1389+
cspParseErrors = errors.Join(cspParseErrors, xerrors.Errorf("invalid CSP header %q, not enough parts to be valid", v))
1390+
continue
1391+
}
1392+
additionalCSPHeaders[httpmw.CSPFetchDirective(strings.ToLower(parts[0]))] = parts[1:]
1393+
}
1394+
1395+
if cspParseErrors != nil {
1396+
// Do not fail Coder deployment startup because of this. Just log an error
1397+
// and continue
1398+
api.Logger.Error(context.Background(),
1399+
"parsing additional CSP headers", slog.Error(cspParseErrors))
1400+
}
1401+
13811402
// Add CSP headers to all static assets and pages. CSP headers only affect
13821403
// browsers, so these don't make sense on api routes.
13831404
cspMW := httpmw.CSPHeaders(options.Telemetry.Enabled(), func() []string {
@@ -1390,7 +1411,7 @@ func New(options *Options) *API {
13901411
}
13911412
// By default we do not add extra websocket connections to the CSP
13921413
return []string{}
1393-
})
1414+
}, additionalCSPHeaders)
13941415

13951416
// Static file handler must be wrapped with HSTS handler if the
13961417
// StrictTransportSecurityAge is set. We only need to set this header on

coderd/httpmw/csp.go

Lines changed: 52 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -23,29 +23,39 @@ func (s cspDirectives) Append(d CSPFetchDirective, values ...string) {
2323
type CSPFetchDirective string
2424

2525
const (
26-
cspDirectiveDefaultSrc = "default-src"
27-
cspDirectiveConnectSrc = "connect-src"
28-
cspDirectiveChildSrc = "child-src"
29-
cspDirectiveScriptSrc = "script-src"
30-
cspDirectiveFontSrc = "font-src"
31-
cspDirectiveStyleSrc = "style-src"
32-
cspDirectiveObjectSrc = "object-src"
33-
cspDirectiveManifestSrc = "manifest-src"
34-
cspDirectiveFrameSrc = "frame-src"
35-
cspDirectiveImgSrc = "img-src"
36-
cspDirectiveReportURI = "report-uri"
37-
cspDirectiveFormAction = "form-action"
38-
cspDirectiveMediaSrc = "media-src"
39-
cspFrameAncestors = "frame-ancestors"
40-
cspDirectiveWorkerSrc = "worker-src"
26+
CSPDirectiveDefaultSrc CSPFetchDirective = "default-src"
27+
CSPDirectiveConnectSrc CSPFetchDirective = "connect-src"
28+
CSPDirectiveChildSrc CSPFetchDirective = "child-src"
29+
CSPDirectiveScriptSrc CSPFetchDirective = "script-src"
30+
CSPDirectiveFontSrc CSPFetchDirective = "font-src"
31+
CSPDirectiveStyleSrc CSPFetchDirective = "style-src"
32+
CSPDirectiveObjectSrc CSPFetchDirective = "object-src"
33+
CSPDirectiveManifestSrc CSPFetchDirective = "manifest-src"
34+
CSPDirectiveFrameSrc CSPFetchDirective = "frame-src"
35+
CSPDirectiveImgSrc CSPFetchDirective = "img-src"
36+
CSPDirectiveReportURI CSPFetchDirective = "report-uri"
37+
CSPDirectiveFormAction CSPFetchDirective = "form-action"
38+
CSPDirectiveMediaSrc CSPFetchDirective = "media-src"
39+
CSPFrameAncestors CSPFetchDirective = "frame-ancestors"
40+
CSPDirectiveWorkerSrc CSPFetchDirective = "worker-src"
4141
)
4242

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

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

8898
if telemetry {
8999
// If telemetry is enabled, we report to coder.com.
90-
cspSrcs.Append(cspDirectiveConnectSrc, "https://coder.com")
100+
cspSrcs.Append(CSPDirectiveConnectSrc, "https://coder.com")
91101
}
92102

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

108118
// The terminal requires a websocket connection to the workspace proxy.
@@ -112,15 +122,19 @@ func CSPHeaders(telemetry bool, websocketHosts func() []string) func(next http.H
112122
for _, extraHost := range extraConnect {
113123
if extraHost == "*" {
114124
// '*' means all
115-
cspSrcs.Append(cspDirectiveConnectSrc, "*")
125+
cspSrcs.Append(CSPDirectiveConnectSrc, "*")
116126
continue
117127
}
118-
cspSrcs.Append(cspDirectiveConnectSrc, fmt.Sprintf("wss://%[1]s ws://%[1]s", extraHost))
128+
cspSrcs.Append(CSPDirectiveConnectSrc, fmt.Sprintf("wss://%[1]s ws://%[1]s", extraHost))
119129
// We also require this to make http/https requests to the workspace proxy for latency checking.
120-
cspSrcs.Append(cspDirectiveConnectSrc, fmt.Sprintf("https://%[1]s http://%[1]s", extraHost))
130+
cspSrcs.Append(CSPDirectiveConnectSrc, fmt.Sprintf("https://%[1]s http://%[1]s", extraHost))
121131
}
122132
}
123133

134+
for directive, values := range staticAdditions {
135+
cspSrcs.Append(directive, values...)
136+
}
137+
124138
var csp strings.Builder
125139
for src, vals := range cspSrcs {
126140
_, _ = fmt.Fprintf(&csp, "%s %s; ", src, strings.Join(vals, " "))

coderd/httpmw/csp_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@ func TestCSPConnect(t *testing.T) {
1515
t.Parallel()
1616

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

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

2223
httpmw.CSPHeaders(false, func() []string {
2324
return expected
25+
}, map[httpmw.CSPFetchDirective][]string{
26+
httpmw.CSPDirectiveMediaSrc: expectedMedia,
2427
})(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
2528
rw.WriteHeader(http.StatusOK)
2629
})).ServeHTTP(rw, r)
@@ -30,4 +33,7 @@ func TestCSPConnect(t *testing.T) {
3033
require.Containsf(t, rw.Header().Get("Content-Security-Policy"), fmt.Sprintf("ws://%s", e), "Content-Security-Policy header should contain ws://%s", e)
3134
require.Containsf(t, rw.Header().Get("Content-Security-Policy"), fmt.Sprintf("wss://%s", e), "Content-Security-Policy header should contain wss://%s", e)
3235
}
36+
for _, e := range expectedMedia {
37+
require.Containsf(t, rw.Header().Get("Content-Security-Policy"), e, "Content-Security-Policy header should contain %s", e)
38+
}
3339
}

codersdk/deployment.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,7 @@ type DeploymentValues struct {
391391
CLIUpgradeMessage serpent.String `json:"cli_upgrade_message,omitempty" typescript:",notnull"`
392392
TermsOfServiceURL serpent.String `json:"terms_of_service_url,omitempty" typescript:",notnull"`
393393
Notifications NotificationsConfig `json:"notifications,omitempty" typescript:",notnull"`
394+
AdditionalCSPPolicy serpent.StringArray `json:"additional_csp_policy,omitempty" typescript:",notnull"`
394395

395396
Config serpent.YAMLConfigPath `json:"config,omitempty" typescript:",notnull"`
396397
WriteConfig serpent.Bool `json:"write_config,omitempty" typescript:",notnull"`
@@ -2147,6 +2148,18 @@ when required by your organization's security policy.`,
21472148
Group: &deploymentGroupIntrospectionLogging,
21482149
YAML: "enableTerraformDebugMode",
21492150
},
2151+
{
2152+
Name: "Additional CSP Policy",
2153+
Description: "Coder configures a Content Security Policy (CSP) to protect against XSS attacks. " +
2154+
"This setting allows you to add additional CSP directives, which can open the attack surface of the deployment. " +
2155+
"Format matches the CSP directive format, e.g. --additional-csp-policy=\"script-src https://example.com\".",
2156+
Flag: "additional-csp-policy",
2157+
Env: "CODER_ADDITIONAL_CSP_POLICY",
2158+
YAML: "additionalCSPPolicy",
2159+
Value: &c.AdditionalCSPPolicy,
2160+
Group: &deploymentGroupNetworkingHTTP,
2161+
},
2162+
21502163
// ☢️ Dangerous settings
21512164
{
21522165
Name: "DANGEROUS: Allow all CORS requests",

docs/reference/api/general.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/reference/api/schemas.md

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy