gitea
Зеркало из https://github.com/go-gitea/gitea
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
20package context
21
22import (
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
32const (
33CsrfHeaderName = "X-Csrf-Token"
34CsrfFormName = "_csrf"
35)
36
37// CSRFProtector represents a CSRF protector and is used to get the current token and validate the token.
38type CSRFProtector interface {
39// PrepareForSessionUser prepares the csrf protector for the current session user.
40PrepareForSessionUser(ctx *Context)
41// Validate validates the csrf token in http context.
42Validate(ctx *Context)
43// DeleteCookie deletes the csrf cookie
44DeleteCookie(ctx *Context)
45}
46
47type csrfProtector struct {
48opt CsrfOptions
49// id must be unique per user.
50id string
51// token is the valid one which wil be used by end user and passed via header, cookie, or hidden form value.
52token string
53}
54
55// CsrfOptions maintains options to manage behavior of Generate.
56type CsrfOptions struct {
57// The global secret value used to generate Tokens.
58Secret string
59// Cookie value used to set and get token.
60Cookie string
61// Cookie domain.
62CookieDomain string
63// Cookie path.
64CookiePath string
65CookieHTTPOnly bool
66// SameSite set the cookie SameSite type
67SameSite http.SameSite
68// Set the Secure flag to true on the cookie.
69Secure bool
70// sessionKey is the key used for getting the unique ID per user.
71sessionKey string
72// oldSessionKey saves old value corresponding to sessionKey.
73oldSessionKey string
74}
75
76func newCsrfCookie(opt *CsrfOptions, value string) *http.Cookie {
77return &http.Cookie{
78Name: opt.Cookie,
79Value: value,
80Path: opt.CookiePath,
81Domain: opt.CookieDomain,
82MaxAge: int(CsrfTokenTimeout.Seconds()),
83Secure: opt.Secure,
84HttpOnly: opt.CookieHTTPOnly,
85SameSite: opt.SameSite,
86}
87}
88
89func NewCSRFProtector(opt CsrfOptions) CSRFProtector {
90if opt.Secret == "" {
91panic("CSRF secret is empty but it must be set") // it shouldn't happen because it is always set in code
92}
93opt.Cookie = util.IfZero(opt.Cookie, "_csrf")
94opt.CookiePath = util.IfZero(opt.CookiePath, "/")
95opt.sessionKey = "uid"
96opt.oldSessionKey = "_old_" + opt.sessionKey
97return &csrfProtector{opt: opt}
98}
99
100func (c *csrfProtector) PrepareForSessionUser(ctx *Context) {
101c.id = "0"
102if uidAny := ctx.Session.Get(c.opt.sessionKey); uidAny != nil {
103switch uidVal := uidAny.(type) {
104case string:
105c.id = uidVal
106case int64:
107c.id = strconv.FormatInt(uidVal, 10)
108default:
109log.Error("invalid uid type in session: %T", uidAny)
110}
111}
112
113oldUID := ctx.Session.Get(c.opt.oldSessionKey)
114uidChanged := oldUID == nil || oldUID.(string) != c.id
115cookieToken := ctx.GetSiteCookie(c.opt.Cookie)
116
117needsNew := true
118if 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.
122if issueTime, ok := ParseCsrfToken(cookieToken); ok {
123dur := time.Since(issueTime) // issueTime is not a monotonic-clock, the server time may change a lot to an early time.
124if dur >= -CsrfTokenRegenerationInterval && dur <= CsrfTokenRegenerationInterval {
125c.token = cookieToken
126needsNew = false
127}
128}
129}
130
131if needsNew {
132// FIXME: actionId.
133c.token = GenerateCsrfToken(c.opt.Secret, c.id, "POST", time.Now())
134cookie := newCsrfCookie(&c.opt, c.token)
135ctx.Resp.Header().Add("Set-Cookie", cookie.String())
136}
137
138ctx.Data["CsrfToken"] = c.token
139ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + template.HTMLEscapeString(c.token) + `">`)
140}
141
142func (c *csrfProtector) validateToken(ctx *Context, token string) {
143if !ValidCsrfToken(token, c.opt.Secret, c.id, "POST", time.Now()) {
144c.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)
147http.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.
154func (c *csrfProtector) Validate(ctx *Context) {
155if token := ctx.Req.Header.Get(CsrfHeaderName); token != "" {
156c.validateToken(ctx, token)
157return
158}
159if token := ctx.Req.FormValue(CsrfFormName); token != "" {
160c.validateToken(ctx, token)
161return
162}
163c.validateToken(ctx, "") // no csrf token, use an empty token to respond error
164}
165
166func (c *csrfProtector) DeleteCookie(ctx *Context) {
167cookie := newCsrfCookie(&c.opt, "")
168cookie.MaxAge = -1
169ctx.Resp.Header().Add("Set-Cookie", cookie.String())
170}
171