|
| 1 | +package httpmw |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "net/http" |
| 6 | + "strings" |
| 7 | +) |
| 8 | + |
| 9 | +// cspDirectives is a map of all csp fetch directives to their values. |
| 10 | +// Each directive is a set of values that is joined by a space (' '). |
| 11 | +// All directives are semi-colon separated as a single string for the csp header. |
| 12 | +type cspDirectives map[CSPFetchDirective][]string |
| 13 | + |
| 14 | +func (s cspDirectives) Append(d CSPFetchDirective, values ...string) { |
| 15 | + if _, ok := s[d]; !ok { |
| 16 | + s[d] = make([]string, 0) |
| 17 | + } |
| 18 | + s[d] = append(s[d], values...) |
| 19 | +} |
| 20 | + |
| 21 | +// CSPFetchDirective is the list of all constant fetch directives that |
| 22 | +// can be used/appended to. |
| 23 | +type CSPFetchDirective string |
| 24 | + |
| 25 | +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" |
| 41 | +) |
| 42 | + |
| 43 | +// 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. |
| 46 | +func CSPHeaders(websocketHosts func() []string) func(next http.Handler) http.Handler { |
| 47 | + return func(next http.Handler) http.Handler { |
| 48 | + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 49 | + // Content-Security-Policy disables loading certain content types and can prevent XSS injections. |
| 50 | + // This site helps eval your policy for syntax and other common issues: https://csp-evaluator.withgoogle.com/ |
| 51 | + // If we ever want to render something like a PDF, we need to adjust "object-src" |
| 52 | + // |
| 53 | + // The list of CSP options: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src |
| 54 | + cspSrcs := cspDirectives{ |
| 55 | + // All omitted fetch csp srcs default to this. |
| 56 | + cspDirectiveDefaultSrc: {"'self'"}, |
| 57 | + cspDirectiveConnectSrc: {"'self'"}, |
| 58 | + cspDirectiveChildSrc: {"'self'"}, |
| 59 | + // https://github.com/suren-atoyan/monaco-react/issues/168 |
| 60 | + cspDirectiveScriptSrc: {"'self'"}, |
| 61 | + cspDirectiveStyleSrc: {"'self' 'unsafe-inline'"}, |
| 62 | + // data: is used by monaco editor on FE for Syntax Highlight |
| 63 | + cspDirectiveFontSrc: {"'self' data:"}, |
| 64 | + cspDirectiveWorkerSrc: {"'self' blob:"}, |
| 65 | + // object-src is needed to support code-server |
| 66 | + cspDirectiveObjectSrc: {"'self'"}, |
| 67 | + // blob: for loading the pwa manifest for code-server |
| 68 | + cspDirectiveManifestSrc: {"'self' blob:"}, |
| 69 | + cspDirectiveFrameSrc: {"'self'"}, |
| 70 | + // data: for loading base64 encoded icons for generic applications. |
| 71 | + // https: allows loading images from external sources. This is not ideal |
| 72 | + // but is required for the templates page that renders readmes. |
| 73 | + // We should find a better solution in the future. |
| 74 | + cspDirectiveImgSrc: {"'self' https: data:"}, |
| 75 | + cspDirectiveFormAction: {"'self'"}, |
| 76 | + cspDirectiveMediaSrc: {"'self'"}, |
| 77 | + // Report all violations back to the server to log |
| 78 | + cspDirectiveReportURI: {"/api/v2/csp/reports"}, |
| 79 | + cspFrameAncestors: {"'none'"}, |
| 80 | + |
| 81 | + // Only scripts can manipulate the dom. This prevents someone from |
| 82 | + // naming themselves something like '<svg onload="alert(/cross-site-scripting/)" />'. |
| 83 | + // "require-trusted-types-for" : []string{"'script'"}, |
| 84 | + } |
| 85 | + |
| 86 | + // This extra connect-src addition is required to support old webkit |
| 87 | + // based browsers (Safari). |
| 88 | + // See issue: https://github.com/w3c/webappsec-csp/issues/7 |
| 89 | + // Once webkit browsers support 'self' on connect-src, we can remove this. |
| 90 | + // When we remove this, the csp header can be static, as opposed to being |
| 91 | + // dynamically generated for each request. |
| 92 | + host := r.Host |
| 93 | + // It is important r.Host is not an empty string. |
| 94 | + if host != "" { |
| 95 | + // We can add both ws:// and wss:// as browsers do not let https |
| 96 | + // pages to connect to non-tls websocket connections. So this |
| 97 | + // supports both http & https webpages. |
| 98 | + cspSrcs.Append(cspDirectiveConnectSrc, fmt.Sprintf("wss://%[1]s ws://%[1]s", host)) |
| 99 | + } |
| 100 | + |
| 101 | + // The terminal requires a websocket connection to the workspace proxy. |
| 102 | + // Make sure we allow this connection to healthy proxies. |
| 103 | + extraConnect := websocketHosts() |
| 104 | + if len(extraConnect) > 0 { |
| 105 | + for _, extraHost := range extraConnect { |
| 106 | + cspSrcs.Append(cspDirectiveConnectSrc, fmt.Sprintf("wss://%[1]s ws://%[1]s", extraHost)) |
| 107 | + } |
| 108 | + } |
| 109 | + |
| 110 | + var csp strings.Builder |
| 111 | + for src, vals := range cspSrcs { |
| 112 | + _, _ = fmt.Fprintf(&csp, "%s %s; ", src, strings.Join(vals, " ")) |
| 113 | + } |
| 114 | + |
| 115 | + w.Header().Set("Content-Security-Policy", csp.String()) |
| 116 | + next.ServeHTTP(w, r) |
| 117 | + }) |
| 118 | + } |
| 119 | +} |
0 commit comments