Skip to content

Commit 3d902c8

Browse files
committed
fix
1 parent f528df9 commit 3d902c8

File tree

3 files changed

+63
-137
lines changed

3 files changed

+63
-137
lines changed

routers/web/web.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ func webAuth(authMethod auth_service.Method) func(*context.Context) {
129129
// ensure the session uid is deleted
130130
_ = ctx.Session.Delete("uid")
131131
}
132+
133+
ctx.Csrf.PrepareForSessionUser(ctx)
132134
}
133135
}
134136

services/context/context.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,8 @@ func Contexter() func(next http.Handler) http.Handler {
138138
csrfOpts := CsrfOptions{
139139
Secret: hex.EncodeToString(setting.GetGeneralTokenSigningSecret()),
140140
Cookie: setting.CSRFCookieName,
141-
SetCookie: true,
142141
Secure: setting.SessionConfig.Secure,
143142
CookieHTTPOnly: setting.CSRFCookieHTTPOnly,
144-
Header: "X-Csrf-Token",
145143
CookieDomain: setting.SessionConfig.Domain,
146144
CookiePath: setting.SessionConfig.CookiePath,
147145
SameSite: setting.SessionConfig.SameSite,
@@ -167,7 +165,7 @@ func Contexter() func(next http.Handler) http.Handler {
167165
ctx.Base.AppendContextValue(WebContextKey, ctx)
168166
ctx.Base.AppendContextValueFunc(gitrepo.RepositoryContextKey, func() any { return ctx.Repo.GitRepo })
169167

170-
ctx.Csrf = PrepareCSRFProtector(csrfOpts, ctx)
168+
ctx.Csrf = NewCSRFProtector(csrfOpts)
171169

172170
// Get the last flash message from cookie
173171
lastFlashCookie := middleware.GetSiteCookie(ctx.Req, CookieNameFlash)
@@ -204,8 +202,6 @@ func Contexter() func(next http.Handler) http.Handler {
204202
ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
205203

206204
ctx.Data["SystemConfig"] = setting.Config()
207-
ctx.Data["CsrfToken"] = ctx.Csrf.GetToken()
208-
ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + ctx.Data["CsrfToken"].(string) + `">`)
209205

210206
// FIXME: do we really always need these setting? There should be someway to have to avoid having to always set these
211207
ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations

services/context/csrf.go

Lines changed: 60 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -20,64 +20,42 @@
2020
package context
2121

2222
import (
23-
"encoding/base32"
24-
"fmt"
23+
"html/template"
2524
"net/http"
2625
"strconv"
2726
"time"
2827

2928
"code.gitea.io/gitea/modules/log"
30-
"code.gitea.io/gitea/modules/setting"
3129
"code.gitea.io/gitea/modules/util"
32-
"code.gitea.io/gitea/modules/web/middleware"
30+
)
31+
32+
const (
33+
CsrfHeaderName = "X-Csrf-Token"
34+
CsrfFormName = "_csrf"
3335
)
3436

3537
// CSRFProtector represents a CSRF protector and is used to get the current token and validate the token.
3638
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.
39+
// PrepareForSessionUser prepares the csrf protector for the current session user.
40+
PrepareForSessionUser(ctx *Context)
41+
// Validate validates the csrf token in http context.
4442
Validate(ctx *Context)
45-
// DeleteCookie deletes the cookie
43+
// DeleteCookie deletes the csrf cookie
4644
DeleteCookie(ctx *Context)
4745
}
4846

4947
type csrfProtector struct {
5048
opt CsrfOptions
51-
// Token generated to pass via header, cookie, or hidden form value.
52-
Token string
53-
// This value must be unique per user.
54-
ID string
55-
}
56-
57-
// GetHeaderName returns the name of the HTTP header for csrf token.
58-
func (c *csrfProtector) GetHeaderName() string {
59-
return c.opt.Header
60-
}
61-
62-
// GetFormName returns the name of the form value for csrf token.
63-
func (c *csrfProtector) GetFormName() string {
64-
return c.opt.Form
65-
}
66-
67-
// GetToken returns the current token. This is typically used
68-
// to populate a hidden form in an HTML template.
69-
func (c *csrfProtector) GetToken() string {
70-
return c.Token
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
7153
}
7254

7355
// CsrfOptions maintains options to manage behavior of Generate.
7456
type CsrfOptions struct {
7557
// The global secret value used to generate Tokens.
7658
Secret string
77-
// HTTP header used to set and get token.
78-
Header string
79-
// Form value used to set and get token.
80-
Form string
8159
// Cookie value used to set and get token.
8260
Cookie string
8361
// Cookie domain.
@@ -87,156 +65,106 @@ type CsrfOptions struct {
8765
CookieHTTPOnly bool
8866
// SameSite set the cookie SameSite type
8967
SameSite http.SameSite
90-
// Key used for getting the unique ID per user.
91-
SessionKey string
92-
// oldSessionKey saves old value corresponding to SessionKey.
93-
oldSessionKey string
94-
// If true, send token via X-Csrf-Token header.
95-
SetHeader bool
96-
// If true, send token via _csrf cookie.
97-
SetCookie bool
9868
// Set the Secure flag to true on the cookie.
9969
Secure bool
100-
// Disallow Origin appear in request header.
101-
Origin bool
102-
// Cookie lifetime. Default is 0
103-
CookieLifeTime int
104-
}
105-
106-
func prepareDefaultCsrfOptions(opt CsrfOptions) CsrfOptions {
107-
if opt.Secret == "" {
108-
randBytes, err := util.CryptoRandomBytes(8)
109-
if err != nil {
110-
// this panic can be handled by the recover() in http handlers
111-
panic(fmt.Errorf("failed to generate random bytes: %w", err))
112-
}
113-
opt.Secret = base32.StdEncoding.EncodeToString(randBytes)
114-
}
115-
if opt.Header == "" {
116-
opt.Header = "X-Csrf-Token"
117-
}
118-
if opt.Form == "" {
119-
opt.Form = "_csrf"
120-
}
121-
if opt.Cookie == "" {
122-
opt.Cookie = "_csrf"
123-
}
124-
if opt.CookiePath == "" {
125-
opt.CookiePath = "/"
126-
}
127-
if opt.SessionKey == "" {
128-
opt.SessionKey = "uid"
129-
}
130-
if opt.CookieLifeTime == 0 {
131-
opt.CookieLifeTime = int(CsrfTokenTimeout.Seconds())
132-
}
133-
134-
opt.oldSessionKey = "_old_" + opt.SessionKey
135-
return opt
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
13674
}
13775

138-
func newCsrfCookie(c *csrfProtector, value string) *http.Cookie {
76+
func newCsrfCookie(opt *CsrfOptions, value string) *http.Cookie {
13977
return &http.Cookie{
140-
Name: c.opt.Cookie,
78+
Name: opt.Cookie,
14179
Value: value,
142-
Path: c.opt.CookiePath,
143-
Domain: c.opt.CookieDomain,
144-
MaxAge: c.opt.CookieLifeTime,
145-
Secure: c.opt.Secure,
146-
HttpOnly: c.opt.CookieHTTPOnly,
147-
SameSite: c.opt.SameSite,
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,
14886
}
14987
}
15088

151-
// PrepareCSRFProtector returns a CSRFProtector to be used for every request.
152-
// Additionally, depending on options set, generated tokens will be sent via Header and/or Cookie.
153-
func PrepareCSRFProtector(opt CsrfOptions, ctx *Context) CSRFProtector {
154-
opt = prepareDefaultCsrfOptions(opt)
155-
x := &csrfProtector{opt: opt}
156-
157-
if opt.Origin && len(ctx.Req.Header.Get("Origin")) > 0 {
158-
return x
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
15992
}
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+
}
16099

161-
x.ID = "0"
162-
uidAny := ctx.Session.Get(opt.SessionKey)
163-
if uidAny != nil {
100+
func (c *csrfProtector) PrepareForSessionUser(ctx *Context) {
101+
c.id = "0"
102+
if uidAny := ctx.Session.Get(c.opt.sessionKey); uidAny != nil {
164103
switch uidVal := uidAny.(type) {
165104
case string:
166-
x.ID = uidVal
105+
c.id = uidVal
167106
case int64:
168-
x.ID = strconv.FormatInt(uidVal, 10)
107+
c.id = strconv.FormatInt(uidVal, 10)
169108
default:
170109
log.Error("invalid uid type in session: %T", uidAny)
171110
}
172111
}
173112

174-
oldUID := ctx.Session.Get(opt.oldSessionKey)
175-
uidChanged := oldUID == nil || oldUID.(string) != x.ID
176-
cookieToken := ctx.GetSiteCookie(opt.Cookie)
113+
oldUID := ctx.Session.Get(c.opt.oldSessionKey)
114+
uidChanged := oldUID == nil || oldUID.(string) != c.id
115+
cookieToken := ctx.GetSiteCookie(c.opt.Cookie)
177116

178117
needsNew := true
179118
if uidChanged {
180-
_ = ctx.Session.Set(opt.oldSessionKey, x.ID)
119+
_ = ctx.Session.Set(c.opt.oldSessionKey, c.id)
181120
} else if cookieToken != "" {
182121
// If cookie token presents, re-use existing unexpired token, else generate a new one.
183122
if issueTime, ok := ParseCsrfToken(cookieToken); ok {
184123
dur := time.Since(issueTime) // issueTime is not a monotonic-clock, the server time may change a lot to an early time.
185124
if dur >= -CsrfTokenRegenerationInterval && dur <= CsrfTokenRegenerationInterval {
186-
x.Token = cookieToken
125+
c.token = cookieToken
187126
needsNew = false
188127
}
189128
}
190129
}
191130

192131
if needsNew {
193132
// FIXME: actionId.
194-
x.Token = GenerateCsrfToken(x.opt.Secret, x.ID, "POST", time.Now())
195-
if opt.SetCookie {
196-
cookie := newCsrfCookie(x, x.Token)
197-
ctx.Resp.Header().Add("Set-Cookie", cookie.String())
198-
}
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())
199136
}
200137

201-
if opt.SetHeader {
202-
ctx.Resp.Header().Add(opt.Header, x.Token)
203-
}
204-
return x
138+
ctx.Data["CsrfToken"] = c.token
139+
ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + c.token + `">`)
205140
}
206141

207142
func (c *csrfProtector) validateToken(ctx *Context, token string) {
208-
if !ValidCsrfToken(token, c.opt.Secret, c.ID, "POST", time.Now()) {
143+
if !ValidCsrfToken(token, c.opt.Secret, c.id, "POST", time.Now()) {
209144
c.DeleteCookie(ctx)
210-
if middleware.IsAPIPath(ctx.Req) {
211-
// currently, there should be no access to the APIPath with CSRF token. because templates shouldn't use the `/api/` endpoints.
212-
http.Error(ctx.Resp, "Invalid CSRF token.", http.StatusBadRequest)
213-
} else {
214-
ctx.Flash.Error(ctx.Tr("error.invalid_csrf"))
215-
ctx.Redirect(setting.AppSubURL + "/")
216-
}
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)
217148
}
218149
}
219150

220151
// Validate should be used as a per route middleware. It attempts to get a token from an "X-Csrf-Token"
221152
// HTTP header and then a "_csrf" form value. If one of these is found, the token will be validated.
222-
// If this validation fails, custom Error is sent in the reply.
223-
// If neither a header nor form value is found, http.StatusBadRequest is sent.
153+
// If this validation fails, http.StatusBadRequest is sent.
224154
func (c *csrfProtector) Validate(ctx *Context) {
225-
if token := ctx.Req.Header.Get(c.GetHeaderName()); token != "" {
155+
if token := ctx.Req.Header.Get(CsrfHeaderName); token != "" {
226156
c.validateToken(ctx, token)
227157
return
228158
}
229-
if token := ctx.Req.FormValue(c.GetFormName()); token != "" {
159+
if token := ctx.Req.FormValue(CsrfFormName); token != "" {
230160
c.validateToken(ctx, token)
231161
return
232162
}
233163
c.validateToken(ctx, "") // no csrf token, use an empty token to respond error
234164
}
235165

236166
func (c *csrfProtector) DeleteCookie(ctx *Context) {
237-
if c.opt.SetCookie {
238-
cookie := newCsrfCookie(c, "")
239-
cookie.MaxAge = -1
240-
ctx.Resp.Header().Add("Set-Cookie", cookie.String())
241-
}
167+
cookie := newCsrfCookie(&c.opt, "")
168+
cookie.MaxAge = -1
169+
ctx.Resp.Header().Add("Set-Cookie", cookie.String())
242170
}

0 commit comments

Comments
 (0)