Skip to content

chore: Dynamic CSP connect-src to support terminals connecting to workspace proxies #7352

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
May 2, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Comment the proxy csp
  • Loading branch information
Emyrk committed May 1, 2023
commit f86b557638ec810a3126df6835a5adf9e8da2fa4
2 changes: 1 addition & 1 deletion coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -822,7 +822,7 @@ type API struct {
WorkspaceClientCoordinateOverride atomic.Pointer[func(rw http.ResponseWriter) bool]
TailnetCoordinator atomic.Pointer[tailnet.Coordinator]
QuotaCommitter atomic.Pointer[proto.QuotaCommitter]
// HealthyWorkspaceProxyHosts returns the hostnames of healthy workspace proxies
// HealthyWorkspaceProxyHosts returns the hosts of healthy workspace proxies
// for header reasons.
HealthyWorkspaceProxyHosts atomic.Pointer[func() []string]
// TemplateScheduleStore is a pointer to an atomic pointer because this is
Expand Down
3 changes: 2 additions & 1 deletion coderd/httpmw/csp.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,11 @@ func CSPHeaders(websocketHosts func() []string) func(next http.Handler) http.Han
cspSrcs.Append(cspDirectiveConnectSrc, fmt.Sprintf("wss://%[1]s ws://%[1]s", host))
}

// The terminal requires a websocket connection to the workspace proxy.
// Make sure we allow this connection to healthy proxies.
extraConnect := websocketHosts()
if len(extraConnect) > 0 {
for _, extraHost := range extraConnect {
fmt.Println("extraHost", extraHost)
cspSrcs.Append(cspDirectiveConnectSrc, fmt.Sprintf("wss://%[1]s ws://%[1]s", extraHost))
}
}
Expand Down
1 change: 0 additions & 1 deletion enterprise/coderd/proxyhealth/proxyhealth.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,6 @@ func (p *ProxyHealth) storeProxyHealth(statuses map[uuid.UUID]ProxyStatus) {

// Store the statuses in the cache before any other quick values.
p.cache.Store(&statuses)
fmt.Println(healthyHosts)
p.heathyHosts.Store(&healthyHosts)
}

Expand Down
100 changes: 1 addition & 99 deletions site/site.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,104 +265,6 @@ func (t *htmlTemplates) renderWithState(filePath string, state htmlState) ([]byt
return buf.Bytes(), nil
}

// CSPDirectives is a map of all csp fetch directives to their values.
// Each directive is a set of values that is joined by a space (' ').
// All directives are semi-colon separated as a single string for the csp header.
type CSPDirectives map[CSPFetchDirective][]string

func (s CSPDirectives) Append(d CSPFetchDirective, values ...string) {
if _, ok := s[d]; !ok {
s[d] = make([]string, 0)
}
s[d] = append(s[d], values...)
}

// CSPFetchDirective is the list of all constant fetch directives that
// can be used/appended to.
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"
)

func cspHeaders(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.
// This site helps eval your policy for syntax and other common issues: https://csp-evaluator.withgoogle.com/
// If we ever want to render something like a PDF, we need to adjust "object-src"
//
// 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'"},
// https://github.com/suren-atoyan/monaco-react/issues/168
CSPDirectiveScriptSrc: {"'self'"},
CSPDirectiveStyleSrc: {"'self' 'unsafe-inline'"},
// data: is used by monaco editor on FE for Syntax Highlight
CSPDirectiveFontSrc: {"'self' data:"},
CSPDirectiveWorkerSrc: {"'self' blob:"},
// object-src is needed to support code-server
CSPDirectiveObjectSrc: {"'self'"},
// blob: for loading the pwa manifest for code-server
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'"},
// Report all violations back to the server to log
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/)" />'.
// "require-trusted-types-for" : []string{"'script'"},
}

// This extra connect-src addition is required to support old webkit
// based browsers (Safari).
// See issue: https://github.com/w3c/webappsec-csp/issues/7
// Once webkit browsers support 'self' on connect-src, we can remove this.
// When we remove this, the csp header can be static, as opposed to being
// dynamically generated for each request.
host := r.Host
// It is important r.Host is not an empty string.
if host != "" {
// 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))
}

var csp strings.Builder
for src, vals := range cspSrcs {
_, _ = fmt.Fprintf(&csp, "%s %s; ", src, strings.Join(vals, " "))
}

w.Header().Set("Content-Security-Policy", csp.String())
next.ServeHTTP(w, r)
})
}

// secureHeaders is only needed for statically served files. We do not need this for api endpoints.
// It adds various headers to enforce browser security features.
func secureHeaders(next http.Handler) http.Handler {
Expand Down Expand Up @@ -394,7 +296,7 @@ func secureHeaders(next http.Handler) http.Handler {

// Prevent the browser from sending Referrer header with requests
ReferrerPolicy: "no-referrer",
}).Handler(cspHeaders(next))
}).Handler(next)
}

// htmlFiles recursively walks the file system passed finding all *.html files.
Expand Down