package httpmw import ( "fmt" "net/http" "strings" "github.com/coder/coder/v2/coderd/proxyhealth" ) // 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 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" CSPFrameSource CSPFetchDirective = "frame-src" CSPDirectiveWorkerSrc CSPFetchDirective = "worker-src" ) // CSPHeaders returns a middleware that sets the Content-Security-Policy header // for coderd. // // Arguments: // - proxyHosts: a function that returns a list of supported proxy hosts // (including the primary). This is to support the terminal connecting to a // workspace proxy and for embedding apps in an iframe. The origin of the // requests do 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, proxyHosts func() []*proxyhealth.ProxyHost, 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. // 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"}, // Only scripts can manipulate the dom. This prevents someone from // naming themselves something like ''. // "require-trusted-types-for" : []string{"'script'"}, } if telemetry { // If telemetry is enabled, we report to coder.com. cspSrcs.Append(CSPDirectiveConnectSrc, "https://coder.com") } // 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)) } // The terminal and iframed apps can use workspace proxies (which includes // the primary). Make sure we allow connections to healthy proxies. extraConnect := proxyHosts() if len(extraConnect) > 0 { for _, extraHost := range extraConnect { // Allow embedding the app host. cspSrcs.Append(CSPDirectiveFrameSrc, extraHost.AppHost) if extraHost.Host == "*" { // '*' means all cspSrcs.Append(CSPDirectiveConnectSrc, "*") continue } // Avoid double-adding r.Host. if extraHost.Host != r.Host { cspSrcs.Append(CSPDirectiveConnectSrc, fmt.Sprintf("wss://%[1]s ws://%[1]s", extraHost.Host)) } // 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.Host)) } } 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, " ")) } w.Header().Set("Content-Security-Policy", csp.String()) next.ServeHTTP(w, r) }) } }