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  }