github.com/google/go-safeweb@v0.0.0-20231219055052-64d8cfc90fbb/safehttp/plugins/xsrf/xsrfangular/xsrf.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 xsrfangular
    16  
    17  import (
    18  	"crypto/rand"
    19  	"encoding/base64"
    20  	"fmt"
    21  	"time"
    22  
    23  	"github.com/google/go-safeweb/safehttp"
    24  	"github.com/google/go-safeweb/safehttp/plugins/xsrf"
    25  )
    26  
    27  // Interceptor provides protection against Cross-Site Request Forgery attacks
    28  // for Angular's XHR requests.
    29  //
    30  // See https://docs.angularjs.org/api/ng/service/$http#cross-site-request-forgery-xsrf-protection for more details.
    31  type Interceptor struct {
    32  	// TokenCookieName is the name of the session cookie that holds the XSRF token.
    33  	TokenCookieName string
    34  	// TokenHeaderName is the name of the HTTP header that holds the XSRF token.
    35  	TokenHeaderName string
    36  }
    37  
    38  var _ safehttp.Interceptor = &Interceptor{}
    39  
    40  // Default creates an Interceptor with TokenCookieName set to XSRF-TOKEN and
    41  // TokenHeaderName set to X-XSRF-TOKEN, their default values. However, in order
    42  // to prevent collisions when multiple applications share the same domain or
    43  // subdomain, each application should set a unique name for the cookie.
    44  //
    45  // See https://docs.angularjs.org/api/ng/service/$http#cross-site-request-forgery-xsrf-protection for more details.
    46  func Default() *Interceptor {
    47  	return &Interceptor{
    48  		TokenCookieName: "XSRF-TOKEN",
    49  		TokenHeaderName: "X-XSRF-TOKEN",
    50  	}
    51  }
    52  
    53  // Before checks for the presence of a matching XSRF token, generated on the
    54  // first page access, in both a cookie and a header. Their names should be set
    55  // when the Interceptor is created.
    56  func (it *Interceptor) Before(w safehttp.ResponseWriter, r *safehttp.IncomingRequest, _ safehttp.InterceptorConfig) safehttp.Result {
    57  	if xsrf.StatePreserving(r) {
    58  		return safehttp.NotWritten()
    59  	}
    60  
    61  	c, err := r.Cookie(it.TokenCookieName)
    62  	if err != nil || c.Value() == "" {
    63  		return w.WriteError(safehttp.StatusForbidden)
    64  	}
    65  
    66  	tok := r.Header.Get(it.TokenHeaderName)
    67  	if tok == "" || tok != c.Value() {
    68  		// JavaScript has access only to cookies from the domain it's running
    69  		// on. Hence, if the same token is found in both the cookie and the
    70  		// header, the request can be trusted.
    71  		return w.WriteError(safehttp.StatusUnauthorized)
    72  	}
    73  
    74  	return safehttp.NotWritten()
    75  }
    76  
    77  func (it *Interceptor) addTokenCookie(w safehttp.ResponseHeadersWriter) error {
    78  	tok := make([]byte, 20)
    79  	if _, err := rand.Read(tok); err != nil {
    80  		return fmt.Errorf("crypto/rand.Read: %v", err)
    81  	}
    82  	c := safehttp.NewCookie(it.TokenCookieName, base64.StdEncoding.EncodeToString(tok))
    83  
    84  	c.SameSite(safehttp.SameSiteStrictMode)
    85  	c.Path("/")
    86  	day := 24 * time.Hour
    87  	c.SetMaxAge(int(day.Seconds()))
    88  	// Needed in order to make the cookie accessible by JavaScript
    89  	// running on the same domain.
    90  	c.DisableHTTPOnly()
    91  
    92  	return w.AddCookie(c)
    93  }
    94  
    95  // Commit generates a cryptographically secure random cookie on the first state
    96  // preserving request (GET, HEAD or OPTION) and sets it in the response. On
    97  // every subsequent request the cookie is expected alongside a header that
    98  // matches its value.
    99  func (it *Interceptor) Commit(w safehttp.ResponseHeadersWriter, r *safehttp.IncomingRequest, resp safehttp.Response, _ safehttp.InterceptorConfig) {
   100  	if c, err := r.Cookie(it.TokenCookieName); err == nil && c.Value() != "" {
   101  		// The XSRF cookie is there so we don't need to do anything else.
   102  		return
   103  	}
   104  
   105  	if !xsrf.StatePreserving(r) {
   106  		// Not a state preserving request, so we won't be adding the cookie.
   107  		return
   108  	}
   109  
   110  	if err := it.addTokenCookie(w); err != nil {
   111  		// This is a server misconfiguration.
   112  		panic("cannot add token cookie")
   113  	}
   114  }
   115  
   116  // Match returns false since there are no supported configurations.
   117  func (*Interceptor) Match(safehttp.InterceptorConfig) bool {
   118  	return false
   119  }