github.com/google/go-safeweb@v0.0.0-20231219055052-64d8cfc90fbb/safehttp/plugins/csp/csp.go (about) 1 // Copyright 2020 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // https://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package csp provides a safehttp.Interceptor which applies Content-Security Policies 16 // to responses. 17 // 18 // These default policies are provided: 19 // - A strict nonce based CSP 20 // - A framing policy which sets frame-ancestors to 'self' 21 // - A Trusted Types policy which makes usage of dangerous web API functions secure by default 22 package csp 23 24 // TODO(empijei): add support for report-to and report groups. 25 26 import ( 27 "context" 28 "encoding/base64" 29 "errors" 30 "fmt" 31 "strings" 32 33 "github.com/google/go-safeweb/safehttp/plugins/csp/internalunsafecsp" 34 "github.com/google/go-safeweb/safehttp/plugins/htmlinject" 35 36 "github.com/google/go-safeweb/safehttp" 37 ) 38 39 const ( 40 responseHeaderKey = "Content-Security-Policy" 41 responseHeaderReportOnlyKey = responseHeaderKey + "-Report-Only" 42 ) 43 44 // nonceSize is the size of the nonces in bytes. According to the CSP3 spec it should 45 // be larger than 16 bytes. 20 bytes was picked to be future proof. 46 // https://www.w3.org/TR/CSP3/#security-nonces 47 const nonceSize = 20 48 49 func generateNonce() string { 50 b := make([]byte, nonceSize) 51 _, err := internalunsafecsp.RandReader.Read(b) 52 if err != nil { 53 panic(fmt.Errorf("failed to generate entropy using crypto/rand/RandReader: %v", err)) 54 } 55 return base64.StdEncoding.EncodeToString(b) 56 } 57 58 type key string 59 60 const ( 61 nonceKey key = "csp-nonce" 62 headersKey key = "csp-headers" 63 ) 64 65 // Nonce retrieves the nonce from the given context. If there is no nonce stored 66 // in the context, an error will be returned. 67 func Nonce(ctx context.Context) (string, error) { 68 v := safehttp.FlightValues(ctx).Get(nonceKey) 69 if v == nil { 70 return "", errors.New("no nonce in context") 71 } 72 return v.(string), nil 73 } 74 75 // nonce retrieves the nonces from the request. 76 // If none is available, one will be generated and added to it. 77 func nonce(r *safehttp.IncomingRequest) string { 78 v := safehttp.FlightValues(r.Context()).Get(nonceKey) 79 var nonce string 80 if v == nil { 81 nonce = generateNonce() 82 safehttp.FlightValues(r.Context()).Put(nonceKey, nonce) 83 } else { 84 nonce = v.(string) 85 } 86 return nonce 87 } 88 89 func claimedHeaders(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) (cspe func([]string), cspro func([]string)) { 90 type claimed struct { 91 cspe, cspro func([]string) 92 } 93 v := safehttp.FlightValues(r.Context()).Get(headersKey) 94 var c claimed 95 if v == nil { 96 h := w.Header() 97 cspe := h.Claim(responseHeaderKey) 98 cspro := h.Claim(responseHeaderReportOnlyKey) 99 c = claimed{cspe: cspe, cspro: cspro} 100 safehttp.FlightValues(r.Context()).Put(headersKey, c) 101 } else { 102 c = v.(claimed) 103 } 104 return c.cspe, c.cspro 105 } 106 107 // Policy defines a CSP policy. 108 type Policy interface { 109 // Serialize serializes this policy for use in a Content-Security-Policy header 110 // or in a Content-Security-Policy-Report-Only header. A nonce will be provided 111 // to Serialize which can be used in 'nonce-{random-nonce}' values in directives. 112 // If a config has matched the interceptor, it will also be passed. 113 Serialize(nonce string, cfg safehttp.InterceptorConfig) string 114 // Match allows to match configurations that are specific to this policy. 115 Match(cfg safehttp.InterceptorConfig) bool 116 // Overridden is used to check if a configuration is overriding the policy. 117 Overridden(cfg safehttp.InterceptorConfig) (disabled, reportOnly bool) 118 } 119 120 func report(reportURI string) string { 121 var b strings.Builder 122 123 if reportURI != "" { 124 b.WriteString("report-uri ") 125 b.WriteString(reportURI) 126 b.WriteString("; ") 127 } 128 129 return b.String() 130 } 131 132 // Interceptor intercepts requests and applies CSP policies. 133 // Multiple interceptors can be installed at the same time. 134 type Interceptor struct { 135 // Policy is the policy the interceptor should enforce. 136 Policy Policy 137 // ReportOnly makes Policy be set report-only. 138 ReportOnly bool 139 } 140 141 var _ safehttp.Interceptor = Interceptor{} 142 143 // Default creates new CSP interceptors with a strict nonce-based policy and a TrustedTypes policy, 144 // all in enforcement mode. 145 // Framing policies are installed by the framing interceptor. 146 func Default(reportURI string) []Interceptor { 147 return []Interceptor{ 148 {Policy: StrictPolicy{ReportURI: reportURI}}, 149 {Policy: TrustedTypesPolicy{ReportURI: reportURI}}, 150 } 151 } 152 153 func (it Interceptor) processOverride(cfg safehttp.InterceptorConfig, nonce string) (enf, ro string) { 154 disabled, reportOnly := false, false 155 if it.Policy.Match(cfg) { 156 disabled, reportOnly = it.Policy.Overridden(cfg) 157 } 158 if disabled { 159 return "", "" 160 } 161 p := it.Policy.Serialize(nonce, cfg) 162 if reportOnly || it.ReportOnly { 163 return "", p 164 } 165 return p, "" 166 } 167 168 // Before claims and sets the Content-Security-Policy header and the 169 // Content-Security-Policy-Report-Only header. 170 func (it Interceptor) Before(w safehttp.ResponseWriter, r *safehttp.IncomingRequest, cfg safehttp.InterceptorConfig) safehttp.Result { 171 nonce := nonce(r) 172 enf, ro := it.processOverride(cfg, nonce) 173 setCSP, setCSPReportOnly := claimedHeaders(w, r) 174 if enf != "" { 175 prev := w.Header().Values(responseHeaderKey) 176 setCSP(append(prev, enf)) 177 } 178 if ro != "" { 179 prev := w.Header().Values(responseHeaderReportOnlyKey) 180 setCSPReportOnly(append(prev, ro)) 181 } 182 return safehttp.NotWritten() 183 } 184 185 // Commit adds the nonce to the safehttp.TemplateResponse which is going to be 186 // injected as the value of the nonce attribute in <script> and <link> tags. The 187 // nonce is going to be unique for each safehttp.IncomingRequest. 188 func (it Interceptor) Commit(w safehttp.ResponseHeadersWriter, r *safehttp.IncomingRequest, resp safehttp.Response, cfg safehttp.InterceptorConfig) { 189 tmplResp, ok := resp.(*safehttp.TemplateResponse) 190 if !ok { 191 return 192 } 193 194 nonce, err := Nonce(r.Context()) 195 if err != nil { 196 // The nonce should have been added in the Before stage and, if that is 197 // not the case, a server misconfiguration occurred. 198 panic("no CSP nonce") 199 } 200 201 if tmplResp.FuncMap == nil { 202 tmplResp.FuncMap = map[string]interface{}{} 203 } 204 tmplResp.FuncMap[htmlinject.CSPNoncesDefaultFuncName] = func() string { return nonce } 205 } 206 207 // Match returns false since there are no supported configurations. 208 func (it Interceptor) Match(cfg safehttp.InterceptorConfig) bool { 209 return it.Policy.Match(cfg) 210 }