20
20
package context
21
21
22
22
import (
23
- "encoding/base32"
24
- "fmt"
23
+ "html/template"
25
24
"net/http"
26
25
"strconv"
27
26
"time"
28
27
29
28
"code.gitea.io/gitea/modules/log"
30
- "code.gitea.io/gitea/modules/setting"
31
29
"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"
33
35
)
34
36
35
37
// CSRFProtector represents a CSRF protector and is used to get the current token and validate the token.
36
38
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.
44
42
Validate (ctx * Context )
45
- // DeleteCookie deletes the cookie
43
+ // DeleteCookie deletes the csrf cookie
46
44
DeleteCookie (ctx * Context )
47
45
}
48
46
49
47
type csrfProtector struct {
50
48
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
71
53
}
72
54
73
55
// CsrfOptions maintains options to manage behavior of Generate.
74
56
type CsrfOptions struct {
75
57
// The global secret value used to generate Tokens.
76
58
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
81
59
// Cookie value used to set and get token.
82
60
Cookie string
83
61
// Cookie domain.
@@ -87,156 +65,106 @@ type CsrfOptions struct {
87
65
CookieHTTPOnly bool
88
66
// SameSite set the cookie SameSite type
89
67
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
98
68
// Set the Secure flag to true on the cookie.
99
69
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
136
74
}
137
75
138
- func newCsrfCookie (c * csrfProtector , value string ) * http.Cookie {
76
+ func newCsrfCookie (opt * CsrfOptions , value string ) * http.Cookie {
139
77
return & http.Cookie {
140
- Name : c . opt .Cookie ,
78
+ Name : opt .Cookie ,
141
79
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 ,
148
86
}
149
87
}
150
88
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
159
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
+ }
160
99
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 {
164
103
switch uidVal := uidAny .(type ) {
165
104
case string :
166
- x . ID = uidVal
105
+ c . id = uidVal
167
106
case int64 :
168
- x . ID = strconv .FormatInt (uidVal , 10 )
107
+ c . id = strconv .FormatInt (uidVal , 10 )
169
108
default :
170
109
log .Error ("invalid uid type in session: %T" , uidAny )
171
110
}
172
111
}
173
112
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 )
177
116
178
117
needsNew := true
179
118
if uidChanged {
180
- _ = ctx .Session .Set (opt .oldSessionKey , x . ID )
119
+ _ = ctx .Session .Set (c . opt .oldSessionKey , c . id )
181
120
} else if cookieToken != "" {
182
121
// If cookie token presents, re-use existing unexpired token, else generate a new one.
183
122
if issueTime , ok := ParseCsrfToken (cookieToken ); ok {
184
123
dur := time .Since (issueTime ) // issueTime is not a monotonic-clock, the server time may change a lot to an early time.
185
124
if dur >= - CsrfTokenRegenerationInterval && dur <= CsrfTokenRegenerationInterval {
186
- x . Token = cookieToken
125
+ c . token = cookieToken
187
126
needsNew = false
188
127
}
189
128
}
190
129
}
191
130
192
131
if needsNew {
193
132
// 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 ())
199
136
}
200
137
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="` + template .HTMLEscapeString (c .token ) + `">` )
205
140
}
206
141
207
142
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 ()) {
209
144
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 )
217
148
}
218
149
}
219
150
220
151
// Validate should be used as a per route middleware. It attempts to get a token from an "X-Csrf-Token"
221
152
// 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.
224
154
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 != "" {
226
156
c .validateToken (ctx , token )
227
157
return
228
158
}
229
- if token := ctx .Req .FormValue (c . GetFormName () ); token != "" {
159
+ if token := ctx .Req .FormValue (CsrfFormName ); token != "" {
230
160
c .validateToken (ctx , token )
231
161
return
232
162
}
233
163
c .validateToken (ctx , "" ) // no csrf token, use an empty token to respond error
234
164
}
235
165
236
166
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 ())
242
170
}
0 commit comments