mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
155 lines
6.5 KiB
Go
155 lines
6.5 KiB
Go
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 '<svg onload="alert(/cross-site-scripting/)" />'.
|
|
// "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)
|
|
})
|
|
}
|
|
}
|