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  }