github.com/google/go-safeweb@v0.0.0-20231219055052-64d8cfc90fbb/safehttp/plugins/xsrf/xsrfhtml/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 xsrfhtml 16 17 import ( 18 "crypto/rand" 19 "encoding/base64" 20 "fmt" 21 22 "github.com/google/go-safeweb/safehttp" 23 "github.com/google/go-safeweb/safehttp/plugins/htmlinject" 24 "github.com/google/go-safeweb/safehttp/plugins/xsrf" 25 "golang.org/x/net/xsrftoken" 26 ) 27 28 const ( 29 // TokenKey is the form key used when sending the token as part of POST 30 // request. 31 TokenKey = "xsrf-token" 32 cookieIDKey = "xsrf-cookie" 33 ) 34 35 // Interceptor implements XSRF protection. 36 type Interceptor struct { 37 // SecretAppKey uniquely identifies each registered service and should have 38 // high entropy as it is used for generating the XSRF token. 39 SecretAppKey string 40 } 41 42 var _ safehttp.Interceptor = &Interceptor{} 43 44 func addCookieID(w safehttp.ResponseHeadersWriter) (*safehttp.Cookie, error) { 45 buf := make([]byte, 20) 46 if _, err := rand.Read(buf); err != nil { 47 return nil, fmt.Errorf("crypto/rand.Read: %v", err) 48 } 49 50 c := safehttp.NewCookie(cookieIDKey, base64.StdEncoding.EncodeToString(buf)) 51 c.SameSite(safehttp.SameSiteStrictMode) 52 if err := w.AddCookie(c); err != nil { 53 return nil, err 54 } 55 return c, nil 56 } 57 58 // Before checks for the presence of a XSRF token in the body of state changing 59 // requests (all except GET, HEAD and OPTIONS) and validates it. 60 func (it *Interceptor) Before(w safehttp.ResponseWriter, r *safehttp.IncomingRequest, _ safehttp.InterceptorConfig) safehttp.Result { 61 if xsrf.StatePreserving(r) { 62 return safehttp.NotWritten() 63 } 64 65 cookieID, err := r.Cookie(cookieIDKey) 66 if err != nil { 67 return w.WriteError(safehttp.StatusForbidden) 68 } 69 70 f, err := r.PostForm() 71 if err != nil { 72 // We fallback to checking whether the form is multipart. Both types 73 // are valid in an incoming request as long as the XSRF token is 74 // present. 75 mf, err := r.MultipartForm(32 << 20) 76 if err != nil { 77 return w.WriteError(safehttp.StatusBadRequest) 78 } 79 f = &mf.Form 80 } 81 82 tok := f.String(TokenKey, "") 83 if f.Err() != nil || tok == "" { 84 return w.WriteError(safehttp.StatusUnauthorized) 85 } 86 87 if ok := xsrftoken.Valid(tok, it.SecretAppKey, cookieID.Value(), r.URL().Host()); !ok { 88 return w.WriteError(safehttp.StatusForbidden) 89 } 90 91 return safehttp.NotWritten() 92 } 93 94 // Commit adds XSRF protection in the response, so the interceptor can 95 // distinguish between subsequent requests coming from an authorized user and 96 // requests that are potentially part of a Cross-Site Request Forgery attack. 97 // 98 // On first user visit through a state preserving request (GET, HEAD or 99 // OPTIONS), a nonce-based cookie is set in the response as a way to 100 // distinguish between users and prevent pre-login XSRF attacks. The cookie is 101 // then used in the token generation and verification algorithm and is expected 102 // to be present in all subsequent incoming requests. 103 // 104 // For every authorized request, the interceptor also generates a 105 // cryptographically-safe XSRF token using the appKey, the cookie and the path 106 // visited. This is then injected as a hidden input field in HTML forms. 107 func (it *Interceptor) Commit(w safehttp.ResponseHeadersWriter, r *safehttp.IncomingRequest, resp safehttp.Response, _ safehttp.InterceptorConfig) { 108 cookieID, err := r.Cookie(cookieIDKey) 109 if err != nil { 110 if !xsrf.StatePreserving(r) { 111 // Not a state preserving request, so we won't be adding the cookie. 112 return 113 } 114 cookieID, err = addCookieID(w) 115 if err != nil { 116 // This is a server misconfiguration. 117 panic("cannot add cookie ID") 118 } 119 } 120 121 tmplResp, ok := resp.(*safehttp.TemplateResponse) 122 if !ok { 123 // If it's not a template response, we cannot inject the token. 124 // TODO: should this be an error? 125 return 126 } 127 128 tok := xsrftoken.Generate(it.SecretAppKey, cookieID.Value(), r.URL().Host()) 129 if tmplResp.FuncMap == nil { 130 tmplResp.FuncMap = map[string]interface{}{} 131 } 132 tmplResp.FuncMap[htmlinject.XSRFTokensDefaultFuncName] = func() string { return tok } 133 } 134 135 // Match returns false since there are no supported configurations. 136 func (*Interceptor) Match(safehttp.InterceptorConfig) bool { 137 return false 138 }