code.gitea.io/gitea@v1.19.3/modules/context/csrf.go (about) 1 // Copyright 2013 Martini Authors 2 // Copyright 2014 The Macaron Authors 3 // Copyright 2021 The Gitea Authors 4 // 5 // Licensed under the Apache License, Version 2.0 (the "License"): you may 6 // not use this file except in compliance with the License. You may obtain 7 // a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, software 12 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 // License for the specific language governing permissions and limitations 15 // under the License. 16 // SPDX-License-Identifier: Apache-2.0 17 18 // a middleware that generates and validates CSRF tokens. 19 20 package context 21 22 import ( 23 "encoding/base32" 24 "fmt" 25 "net/http" 26 "strconv" 27 "time" 28 29 "code.gitea.io/gitea/modules/log" 30 "code.gitea.io/gitea/modules/setting" 31 "code.gitea.io/gitea/modules/util" 32 "code.gitea.io/gitea/modules/web/middleware" 33 ) 34 35 // CSRFProtector represents a CSRF protector and is used to get the current token and validate the token. 36 type CSRFProtector interface { 37 // GetHeaderName returns HTTP header to search for token. 38 GetHeaderName() string 39 // GetFormName returns form value to search for token. 40 GetFormName() string 41 // GetToken returns the token. 42 GetToken() string 43 // Validate validates the token in http context. 44 Validate(ctx *Context) 45 } 46 47 type csrfProtector struct { 48 // Header name value for setting and getting csrf token. 49 Header string 50 // Form name value for setting and getting csrf token. 51 Form string 52 // Cookie name value for setting and getting csrf token. 53 Cookie string 54 // Cookie domain 55 CookieDomain string 56 // Cookie path 57 CookiePath string 58 // Cookie HttpOnly flag value used for the csrf token. 59 CookieHTTPOnly bool 60 // Token generated to pass via header, cookie, or hidden form value. 61 Token string 62 // This value must be unique per user. 63 ID string 64 // Secret used along with the unique id above to generate the Token. 65 Secret string 66 } 67 68 // GetHeaderName returns the name of the HTTP header for csrf token. 69 func (c *csrfProtector) GetHeaderName() string { 70 return c.Header 71 } 72 73 // GetFormName returns the name of the form value for csrf token. 74 func (c *csrfProtector) GetFormName() string { 75 return c.Form 76 } 77 78 // GetToken returns the current token. This is typically used 79 // to populate a hidden form in an HTML template. 80 func (c *csrfProtector) GetToken() string { 81 return c.Token 82 } 83 84 // CsrfOptions maintains options to manage behavior of Generate. 85 type CsrfOptions struct { 86 // The global secret value used to generate Tokens. 87 Secret string 88 // HTTP header used to set and get token. 89 Header string 90 // Form value used to set and get token. 91 Form string 92 // Cookie value used to set and get token. 93 Cookie string 94 // Cookie domain. 95 CookieDomain string 96 // Cookie path. 97 CookiePath string 98 CookieHTTPOnly bool 99 // SameSite set the cookie SameSite type 100 SameSite http.SameSite 101 // Key used for getting the unique ID per user. 102 SessionKey string 103 // oldSessionKey saves old value corresponding to SessionKey. 104 oldSessionKey string 105 // If true, send token via X-Csrf-Token header. 106 SetHeader bool 107 // If true, send token via _csrf cookie. 108 SetCookie bool 109 // Set the Secure flag to true on the cookie. 110 Secure bool 111 // Disallow Origin appear in request header. 112 Origin bool 113 // Cookie lifetime. Default is 0 114 CookieLifeTime int 115 } 116 117 func prepareDefaultCsrfOptions(opt CsrfOptions) CsrfOptions { 118 if opt.Secret == "" { 119 randBytes, err := util.CryptoRandomBytes(8) 120 if err != nil { 121 // this panic can be handled by the recover() in http handlers 122 panic(fmt.Errorf("failed to generate random bytes: %w", err)) 123 } 124 opt.Secret = base32.StdEncoding.EncodeToString(randBytes) 125 } 126 if opt.Header == "" { 127 opt.Header = "X-Csrf-Token" 128 } 129 if opt.Form == "" { 130 opt.Form = "_csrf" 131 } 132 if opt.Cookie == "" { 133 opt.Cookie = "_csrf" 134 } 135 if opt.CookiePath == "" { 136 opt.CookiePath = "/" 137 } 138 if opt.SessionKey == "" { 139 opt.SessionKey = "uid" 140 } 141 opt.oldSessionKey = "_old_" + opt.SessionKey 142 return opt 143 } 144 145 // PrepareCSRFProtector returns a CSRFProtector to be used for every request. 146 // Additionally, depending on options set, generated tokens will be sent via Header and/or Cookie. 147 func PrepareCSRFProtector(opt CsrfOptions, ctx *Context) CSRFProtector { 148 opt = prepareDefaultCsrfOptions(opt) 149 x := &csrfProtector{ 150 Secret: opt.Secret, 151 Header: opt.Header, 152 Form: opt.Form, 153 Cookie: opt.Cookie, 154 CookieDomain: opt.CookieDomain, 155 CookiePath: opt.CookiePath, 156 CookieHTTPOnly: opt.CookieHTTPOnly, 157 } 158 159 if opt.Origin && len(ctx.Req.Header.Get("Origin")) > 0 { 160 return x 161 } 162 163 x.ID = "0" 164 uidAny := ctx.Session.Get(opt.SessionKey) 165 if uidAny != nil { 166 switch uidVal := uidAny.(type) { 167 case string: 168 x.ID = uidVal 169 case int64: 170 x.ID = strconv.FormatInt(uidVal, 10) 171 default: 172 log.Error("invalid uid type in session: %T", uidAny) 173 } 174 } 175 176 oldUID := ctx.Session.Get(opt.oldSessionKey) 177 uidChanged := oldUID == nil || oldUID.(string) != x.ID 178 cookieToken := ctx.GetCookie(opt.Cookie) 179 180 needsNew := true 181 if uidChanged { 182 _ = ctx.Session.Set(opt.oldSessionKey, x.ID) 183 } else if cookieToken != "" { 184 // If cookie token presents, re-use existing unexpired token, else generate a new one. 185 if issueTime, ok := ParseCsrfToken(cookieToken); ok { 186 dur := time.Since(issueTime) // issueTime is not a monotonic-clock, the server time may change a lot to an early time. 187 if dur >= -CsrfTokenRegenerationInterval && dur <= CsrfTokenRegenerationInterval { 188 x.Token = cookieToken 189 needsNew = false 190 } 191 } 192 } 193 194 if needsNew { 195 // FIXME: actionId. 196 x.Token = GenerateCsrfToken(x.Secret, x.ID, "POST", time.Now()) 197 if opt.SetCookie { 198 var expires interface{} 199 if opt.CookieLifeTime == 0 { 200 expires = time.Now().Add(CsrfTokenTimeout) 201 } 202 middleware.SetCookie(ctx.Resp, opt.Cookie, x.Token, 203 opt.CookieLifeTime, 204 opt.CookiePath, 205 opt.CookieDomain, 206 opt.Secure, 207 opt.CookieHTTPOnly, 208 expires, 209 middleware.SameSite(opt.SameSite), 210 ) 211 } 212 } 213 214 if opt.SetHeader { 215 ctx.Resp.Header().Add(opt.Header, x.Token) 216 } 217 return x 218 } 219 220 func (c *csrfProtector) validateToken(ctx *Context, token string) { 221 if !ValidCsrfToken(token, c.Secret, c.ID, "POST", time.Now()) { 222 middleware.DeleteCSRFCookie(ctx.Resp) 223 if middleware.IsAPIPath(ctx.Req) { 224 // currently, there should be no access to the APIPath with CSRF token. because templates shouldn't use the `/api/` endpoints. 225 http.Error(ctx.Resp, "Invalid CSRF token.", http.StatusBadRequest) 226 } else { 227 ctx.Flash.Error(ctx.Tr("error.invalid_csrf")) 228 ctx.Redirect(setting.AppSubURL + "/") 229 } 230 } 231 } 232 233 // Validate should be used as a per route middleware. It attempts to get a token from an "X-Csrf-Token" 234 // HTTP header and then a "_csrf" form value. If one of these is found, the token will be validated. 235 // If this validation fails, custom Error is sent in the reply. 236 // If neither a header nor form value is found, http.StatusBadRequest is sent. 237 func (c *csrfProtector) Validate(ctx *Context) { 238 if token := ctx.Req.Header.Get(c.GetHeaderName()); token != "" { 239 c.validateToken(ctx, token) 240 return 241 } 242 if token := ctx.Req.FormValue(c.GetFormName()); token != "" { 243 c.validateToken(ctx, token) 244 return 245 } 246 c.validateToken(ctx, "") // no csrf token, use an empty token to respond error 247 }