code.gitea.io/gitea@v1.22.3/services/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 "html/template" 24 "net/http" 25 "strconv" 26 "time" 27 28 "code.gitea.io/gitea/modules/log" 29 "code.gitea.io/gitea/modules/util" 30 ) 31 32 const ( 33 CsrfHeaderName = "X-Csrf-Token" 34 CsrfFormName = "_csrf" 35 ) 36 37 // CSRFProtector represents a CSRF protector and is used to get the current token and validate the token. 38 type CSRFProtector interface { 39 // PrepareForSessionUser prepares the csrf protector for the current session user. 40 PrepareForSessionUser(ctx *Context) 41 // Validate validates the csrf token in http context. 42 Validate(ctx *Context) 43 // DeleteCookie deletes the csrf cookie 44 DeleteCookie(ctx *Context) 45 } 46 47 type csrfProtector struct { 48 opt CsrfOptions 49 // id must be unique per user. 50 id string 51 // token is the valid one which wil be used by end user and passed via header, cookie, or hidden form value. 52 token string 53 } 54 55 // CsrfOptions maintains options to manage behavior of Generate. 56 type CsrfOptions struct { 57 // The global secret value used to generate Tokens. 58 Secret string 59 // Cookie value used to set and get token. 60 Cookie string 61 // Cookie domain. 62 CookieDomain string 63 // Cookie path. 64 CookiePath string 65 CookieHTTPOnly bool 66 // SameSite set the cookie SameSite type 67 SameSite http.SameSite 68 // Set the Secure flag to true on the cookie. 69 Secure bool 70 // sessionKey is the key used for getting the unique ID per user. 71 sessionKey string 72 // oldSessionKey saves old value corresponding to sessionKey. 73 oldSessionKey string 74 } 75 76 func newCsrfCookie(opt *CsrfOptions, value string) *http.Cookie { 77 return &http.Cookie{ 78 Name: opt.Cookie, 79 Value: value, 80 Path: opt.CookiePath, 81 Domain: opt.CookieDomain, 82 MaxAge: int(CsrfTokenTimeout.Seconds()), 83 Secure: opt.Secure, 84 HttpOnly: opt.CookieHTTPOnly, 85 SameSite: opt.SameSite, 86 } 87 } 88 89 func NewCSRFProtector(opt CsrfOptions) CSRFProtector { 90 if opt.Secret == "" { 91 panic("CSRF secret is empty but it must be set") // it shouldn't happen because it is always set in code 92 } 93 opt.Cookie = util.IfZero(opt.Cookie, "_csrf") 94 opt.CookiePath = util.IfZero(opt.CookiePath, "/") 95 opt.sessionKey = "uid" 96 opt.oldSessionKey = "_old_" + opt.sessionKey 97 return &csrfProtector{opt: opt} 98 } 99 100 func (c *csrfProtector) PrepareForSessionUser(ctx *Context) { 101 c.id = "0" 102 if uidAny := ctx.Session.Get(c.opt.sessionKey); uidAny != nil { 103 switch uidVal := uidAny.(type) { 104 case string: 105 c.id = uidVal 106 case int64: 107 c.id = strconv.FormatInt(uidVal, 10) 108 default: 109 log.Error("invalid uid type in session: %T", uidAny) 110 } 111 } 112 113 oldUID := ctx.Session.Get(c.opt.oldSessionKey) 114 uidChanged := oldUID == nil || oldUID.(string) != c.id 115 cookieToken := ctx.GetSiteCookie(c.opt.Cookie) 116 117 needsNew := true 118 if uidChanged { 119 _ = ctx.Session.Set(c.opt.oldSessionKey, c.id) 120 } else if cookieToken != "" { 121 // If cookie token presents, re-use existing unexpired token, else generate a new one. 122 if issueTime, ok := ParseCsrfToken(cookieToken); ok { 123 dur := time.Since(issueTime) // issueTime is not a monotonic-clock, the server time may change a lot to an early time. 124 if dur >= -CsrfTokenRegenerationInterval && dur <= CsrfTokenRegenerationInterval { 125 c.token = cookieToken 126 needsNew = false 127 } 128 } 129 } 130 131 if needsNew { 132 // FIXME: actionId. 133 c.token = GenerateCsrfToken(c.opt.Secret, c.id, "POST", time.Now()) 134 cookie := newCsrfCookie(&c.opt, c.token) 135 ctx.Resp.Header().Add("Set-Cookie", cookie.String()) 136 } 137 138 ctx.Data["CsrfToken"] = c.token 139 ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + template.HTMLEscapeString(c.token) + `">`) 140 } 141 142 func (c *csrfProtector) validateToken(ctx *Context, token string) { 143 if !ValidCsrfToken(token, c.opt.Secret, c.id, "POST", time.Now()) { 144 c.DeleteCookie(ctx) 145 // currently, there should be no access to the APIPath with CSRF token. because templates shouldn't use the `/api/` endpoints. 146 // FIXME: distinguish what the response is for: HTML (web page) or JSON (fetch) 147 http.Error(ctx.Resp, "Invalid CSRF token.", http.StatusBadRequest) 148 } 149 } 150 151 // Validate should be used as a per route middleware. It attempts to get a token from an "X-Csrf-Token" 152 // HTTP header and then a "_csrf" form value. If one of these is found, the token will be validated. 153 // If this validation fails, http.StatusBadRequest is sent. 154 func (c *csrfProtector) Validate(ctx *Context) { 155 if token := ctx.Req.Header.Get(CsrfHeaderName); token != "" { 156 c.validateToken(ctx, token) 157 return 158 } 159 if token := ctx.Req.FormValue(CsrfFormName); token != "" { 160 c.validateToken(ctx, token) 161 return 162 } 163 c.validateToken(ctx, "") // no csrf token, use an empty token to respond error 164 } 165 166 func (c *csrfProtector) DeleteCookie(ctx *Context) { 167 cookie := newCsrfCookie(&c.opt, "") 168 cookie.MaxAge = -1 169 ctx.Resp.Header().Add("Set-Cookie", cookie.String()) 170 }