Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit9b5ee8f

Browse files
Emyrkpresleyp
andauthored
feat: Implement (but not enforce) CSRF for FE requests (#3786)
Future work is to enforce CSRFCo-authored-by: Presley Pizzo <presley@coder.com>
1 parent9ab437d commit9b5ee8f

22 files changed

+211
-115
lines changed

‎coderd/coderd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ func New(options *Options) *API {
187187
next.ServeHTTP(w,r)
188188
})
189189
},
190+
httpmw.CSRF(options.SecureAuthCookie),
190191
)
191192

192193
apps:=func(r chi.Router) {

‎coderd/httpapi/cookie_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ func TestStripCoderCookies(t *testing.T) {
1717
"testing=hello; wow=test",
1818
"testing=hello; wow=test",
1919
}, {
20-
"session_token=moo; wow=test",
20+
"coder_session_token=moo; wow=test",
2121
"wow=test",
2222
}, {
23-
"another_token=wow;session_token=ok",
23+
"another_token=wow;coder_session_token=ok",
2424
"another_token=wow",
2525
}, {
26-
"session_token=ok; oauth_state=wow; oauth_redirect=/",
26+
"coder_session_token=ok; oauth_state=wow; oauth_redirect=/",
2727
"",
2828
}} {
2929
tc:=tc

‎coderd/httpmw/apikey.go

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -123,13 +123,7 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool
123123
httpapi.Write(rw,code,response)
124124
}
125125

126-
varcookieValuestring
127-
cookie,err:=r.Cookie(codersdk.SessionTokenKey)
128-
iferr!=nil {
129-
cookieValue=r.URL.Query().Get(codersdk.SessionTokenKey)
130-
}else {
131-
cookieValue=cookie.Value
132-
}
126+
cookieValue:=apiTokenFromRequest(r)
133127
ifcookieValue=="" {
134128
write(http.StatusUnauthorized, codersdk.Response{
135129
Message:signedOutErrorMessage,
@@ -335,3 +329,36 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool
335329
})
336330
}
337331
}
332+
333+
// apiTokenFromRequest returns the api token from the request.
334+
// Find the session token from:
335+
// 1: The cookie
336+
// 2: The old cookie
337+
// 3. The coder_session_token query parameter
338+
// 4. The custom auth header
339+
funcapiTokenFromRequest(r*http.Request)string {
340+
cookie,err:=r.Cookie(codersdk.SessionTokenKey)
341+
iferr==nil&&cookie.Value!="" {
342+
returncookie.Value
343+
}
344+
345+
// TODO: @emyrk in October 2022, remove this oldCookie check.
346+
//This is just to support the old cli for 1 release. Then everyone
347+
//must update.
348+
oldCookie,err:=r.Cookie("session_token")
349+
iferr==nil&&oldCookie.Value!="" {
350+
returnoldCookie.Value
351+
}
352+
353+
urlValue:=r.URL.Query().Get(codersdk.SessionTokenKey)
354+
ifurlValue!="" {
355+
returnurlValue
356+
}
357+
358+
headerValue:=r.Header.Get(codersdk.SessionCustomHeader)
359+
ifheaderValue!="" {
360+
returnheaderValue
361+
}
362+
363+
return""
364+
}

‎coderd/httpmw/apikey_test.go

Lines changed: 12 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,7 @@ func TestAPIKey(t *testing.T) {
7474
r=httptest.NewRequest("GET","/",nil)
7575
rw=httptest.NewRecorder()
7676
)
77-
r.AddCookie(&http.Cookie{
78-
Name:codersdk.SessionTokenKey,
79-
Value:"test-wow-hello",
80-
})
77+
r.Header.Set(codersdk.SessionCustomHeader,"test-wow-hello")
8178

8279
httpmw.ExtractAPIKey(db,nil,false)(successHandler).ServeHTTP(rw,r)
8380
res:=rw.Result()
@@ -92,10 +89,7 @@ func TestAPIKey(t *testing.T) {
9289
r=httptest.NewRequest("GET","/",nil)
9390
rw=httptest.NewRecorder()
9491
)
95-
r.AddCookie(&http.Cookie{
96-
Name:codersdk.SessionTokenKey,
97-
Value:"test-wow",
98-
})
92+
r.Header.Set(codersdk.SessionCustomHeader,"test-wow")
9993

10094
httpmw.ExtractAPIKey(db,nil,false)(successHandler).ServeHTTP(rw,r)
10195
res:=rw.Result()
@@ -110,10 +104,7 @@ func TestAPIKey(t *testing.T) {
110104
r=httptest.NewRequest("GET","/",nil)
111105
rw=httptest.NewRecorder()
112106
)
113-
r.AddCookie(&http.Cookie{
114-
Name:codersdk.SessionTokenKey,
115-
Value:"testtestid-wow",
116-
})
107+
r.Header.Set(codersdk.SessionCustomHeader,"testtestid-wow")
117108

118109
httpmw.ExtractAPIKey(db,nil,false)(successHandler).ServeHTTP(rw,r)
119110
res:=rw.Result()
@@ -129,10 +120,7 @@ func TestAPIKey(t *testing.T) {
129120
r=httptest.NewRequest("GET","/",nil)
130121
rw=httptest.NewRecorder()
131122
)
132-
r.AddCookie(&http.Cookie{
133-
Name:codersdk.SessionTokenKey,
134-
Value:fmt.Sprintf("%s-%s",id,secret),
135-
})
123+
r.Header.Set(codersdk.SessionCustomHeader,fmt.Sprintf("%s-%s",id,secret))
136124

137125
httpmw.ExtractAPIKey(db,nil,false)(successHandler).ServeHTTP(rw,r)
138126
res:=rw.Result()
@@ -149,10 +137,7 @@ func TestAPIKey(t *testing.T) {
149137
rw=httptest.NewRecorder()
150138
user=createUser(r.Context(),t,db)
151139
)
152-
r.AddCookie(&http.Cookie{
153-
Name:codersdk.SessionTokenKey,
154-
Value:fmt.Sprintf("%s-%s",id,secret),
155-
})
140+
r.Header.Set(codersdk.SessionCustomHeader,fmt.Sprintf("%s-%s",id,secret))
156141

157142
// Use a different secret so they don't match!
158143
hashed:=sha256.Sum256([]byte("differentsecret"))
@@ -178,10 +163,7 @@ func TestAPIKey(t *testing.T) {
178163
rw=httptest.NewRecorder()
179164
user=createUser(r.Context(),t,db)
180165
)
181-
r.AddCookie(&http.Cookie{
182-
Name:codersdk.SessionTokenKey,
183-
Value:fmt.Sprintf("%s-%s",id,secret),
184-
})
166+
r.Header.Set(codersdk.SessionCustomHeader,fmt.Sprintf("%s-%s",id,secret))
185167

186168
_,err:=db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
187169
ID:id,
@@ -206,10 +188,7 @@ func TestAPIKey(t *testing.T) {
206188
rw=httptest.NewRecorder()
207189
user=createUser(r.Context(),t,db)
208190
)
209-
r.AddCookie(&http.Cookie{
210-
Name:codersdk.SessionTokenKey,
211-
Value:fmt.Sprintf("%s-%s",id,secret),
212-
})
191+
r.Header.Set(codersdk.SessionCustomHeader,fmt.Sprintf("%s-%s",id,secret))
213192

214193
sentAPIKey,err:=db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
215194
ID:id,
@@ -280,10 +259,7 @@ func TestAPIKey(t *testing.T) {
280259
rw=httptest.NewRecorder()
281260
user=createUser(r.Context(),t,db)
282261
)
283-
r.AddCookie(&http.Cookie{
284-
Name:codersdk.SessionTokenKey,
285-
Value:fmt.Sprintf("%s-%s",id,secret),
286-
})
262+
r.Header.Set(codersdk.SessionCustomHeader,fmt.Sprintf("%s-%s",id,secret))
287263

288264
sentAPIKey,err:=db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
289265
ID:id,
@@ -316,10 +292,7 @@ func TestAPIKey(t *testing.T) {
316292
rw=httptest.NewRecorder()
317293
user=createUser(r.Context(),t,db)
318294
)
319-
r.AddCookie(&http.Cookie{
320-
Name:codersdk.SessionTokenKey,
321-
Value:fmt.Sprintf("%s-%s",id,secret),
322-
})
295+
r.Header.Set(codersdk.SessionCustomHeader,fmt.Sprintf("%s-%s",id,secret))
323296

324297
sentAPIKey,err:=db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
325298
ID:id,
@@ -352,10 +325,7 @@ func TestAPIKey(t *testing.T) {
352325
rw=httptest.NewRecorder()
353326
user=createUser(r.Context(),t,db)
354327
)
355-
r.AddCookie(&http.Cookie{
356-
Name:codersdk.SessionTokenKey,
357-
Value:fmt.Sprintf("%s-%s",id,secret),
358-
})
328+
r.Header.Set(codersdk.SessionCustomHeader,fmt.Sprintf("%s-%s",id,secret))
359329

360330
sentAPIKey,err:=db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
361331
ID:id,
@@ -395,10 +365,7 @@ func TestAPIKey(t *testing.T) {
395365
rw=httptest.NewRecorder()
396366
user=createUser(r.Context(),t,db)
397367
)
398-
r.AddCookie(&http.Cookie{
399-
Name:codersdk.SessionTokenKey,
400-
Value:fmt.Sprintf("%s-%s",id,secret),
401-
})
368+
r.Header.Set(codersdk.SessionCustomHeader,fmt.Sprintf("%s-%s",id,secret))
402369

403370
sentAPIKey,err:=db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
404371
ID:id,
@@ -449,10 +416,7 @@ func TestAPIKey(t *testing.T) {
449416
user=createUser(r.Context(),t,db)
450417
)
451418
r.RemoteAddr="1.1.1.1:3555"
452-
r.AddCookie(&http.Cookie{
453-
Name:codersdk.SessionTokenKey,
454-
Value:fmt.Sprintf("%s-%s",id,secret),
455-
})
419+
r.Header.Set(codersdk.SessionCustomHeader,fmt.Sprintf("%s-%s",id,secret))
456420

457421
_,err:=db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
458422
ID:id,

‎coderd/httpmw/authorize_test.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,7 @@ func TestExtractUserRoles(t *testing.T) {
9393
})
9494

9595
req:=httptest.NewRequest("GET","/",nil)
96-
req.AddCookie(&http.Cookie{
97-
Name:codersdk.SessionTokenKey,
98-
Value:token,
99-
})
96+
req.Header.Set(codersdk.SessionCustomHeader,token)
10097

10198
rtr.ServeHTTP(rw,req)
10299
resp:=rw.Result()

‎coderd/httpmw/csrf.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package httpmw
2+
3+
import (
4+
"net/http"
5+
"regexp"
6+
7+
"github.com/justinas/nosurf"
8+
"golang.org/x/xerrors"
9+
10+
"github.com/coder/coder/codersdk"
11+
)
12+
13+
// CSRF is a middleware that verifies that a CSRF token is present in the request
14+
// for non-GET requests.
15+
funcCSRF(secureCookiebool)func(next http.Handler) http.Handler {
16+
returnfunc(next http.Handler) http.Handler {
17+
mw:=nosurf.New(next)
18+
mw.SetBaseCookie(http.Cookie{Path:"/",HttpOnly:true,SameSite:http.SameSiteLaxMode,Secure:secureCookie})
19+
mw.SetFailureHandler(http.HandlerFunc(func(w http.ResponseWriter,r*http.Request) {
20+
http.Error(w,"Something is wrong with your CSRF token. Please refresh the page. If this error persists, try clearing your cookies.",http.StatusBadRequest)
21+
}))
22+
23+
// Exempt all requests that do not require CSRF protection.
24+
// All GET requests are exempt by default.
25+
mw.ExemptPath("/api/v2/csp/reports")
26+
27+
// Top level agent routes.
28+
mw.ExemptRegexp(regexp.MustCompile("api/v2/workspaceagents/[^/]*$"))
29+
// Agent authenticated routes
30+
mw.ExemptRegexp(regexp.MustCompile("api/v2/workspaceagents/me/*"))
31+
// Derp routes
32+
mw.ExemptRegexp(regexp.MustCompile("derp/*"))
33+
34+
mw.ExemptFunc(func(r*http.Request)bool {
35+
// Enable CSRF in November 2022 by deleting this "return true" line.
36+
// CSRF is not enforced to ensure backwards compatibility with older
37+
// cli versions.
38+
//nolint:revive
39+
returntrue
40+
41+
// CSRF only affects requests that automatically attach credentials via a cookie.
42+
// If no cookie is present, then there is no risk of CSRF.
43+
//nolint:govet
44+
sessCookie,err:=r.Cookie(codersdk.SessionTokenKey)
45+
ifxerrors.Is(err,http.ErrNoCookie) {
46+
returntrue
47+
}
48+
49+
iftoken:=r.Header.Get(codersdk.SessionCustomHeader);token==sessCookie.Value {
50+
// If the cookie and header match, we can assume this is the same as just using the
51+
// custom header auth. Custom header auth can bypass CSRF, as CSRF attacks
52+
// cannot add custom headers.
53+
returntrue
54+
}
55+
56+
iftoken:=r.URL.Query().Get(codersdk.SessionTokenKey);token==sessCookie.Value {
57+
// If the auth is set in a url param and matches the cookie, it
58+
// is the same as just using the url param.
59+
returntrue
60+
}
61+
62+
// If the X-CSRF-TOKEN header is set, we can exempt the func if it's valid.
63+
// This is the CSRF check.
64+
sent:=r.Header.Get("X-CSRF-TOKEN")
65+
ifsent!="" {
66+
returnnosurf.VerifyToken(nosurf.Token(r),sent)
67+
}
68+
returnfalse
69+
})
70+
returnmw
71+
}
72+
}

‎coderd/httpmw/organizationparam_test.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,7 @@ func TestOrganizationParam(t *testing.T) {
2929
r=httptest.NewRequest("GET","/",nil)
3030
hashed=sha256.Sum256([]byte(secret))
3131
)
32-
r.AddCookie(&http.Cookie{
33-
Name:codersdk.SessionTokenKey,
34-
Value:fmt.Sprintf("%s-%s",id,secret),
35-
})
32+
r.Header.Set(codersdk.SessionCustomHeader,fmt.Sprintf("%s-%s",id,secret))
3633

3734
userID:=uuid.New()
3835
username,err:=cryptorand.String(8)

‎coderd/httpmw/templateparam_test.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,7 @@ func TestTemplateParam(t *testing.T) {
2929
hashed=sha256.Sum256([]byte(secret))
3030
)
3131
r:=httptest.NewRequest("GET","/",nil)
32-
r.AddCookie(&http.Cookie{
33-
Name:codersdk.SessionTokenKey,
34-
Value:fmt.Sprintf("%s-%s",id,secret),
35-
})
32+
r.Header.Set(codersdk.SessionCustomHeader,fmt.Sprintf("%s-%s",id,secret))
3633

3734
userID:=uuid.New()
3835
username,err:=cryptorand.String(8)

‎coderd/httpmw/templateversionparam_test.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,7 @@ func TestTemplateVersionParam(t *testing.T) {
2929
hashed=sha256.Sum256([]byte(secret))
3030
)
3131
r:=httptest.NewRequest("GET","/",nil)
32-
r.AddCookie(&http.Cookie{
33-
Name:codersdk.SessionTokenKey,
34-
Value:fmt.Sprintf("%s-%s",id,secret),
35-
})
32+
r.Header.Set(codersdk.SessionCustomHeader,fmt.Sprintf("%s-%s",id,secret))
3633

3734
userID:=uuid.New()
3835
username,err:=cryptorand.String(8)

‎coderd/httpmw/userparam_test.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,7 @@ func TestUserParam(t *testing.T) {
2929
r=httptest.NewRequest("GET","/",nil)
3030
rw=httptest.NewRecorder()
3131
)
32-
r.AddCookie(&http.Cookie{
33-
Name:codersdk.SessionTokenKey,
34-
Value:fmt.Sprintf("%s-%s",id,secret),
35-
})
32+
r.Header.Set(codersdk.SessionCustomHeader,fmt.Sprintf("%s-%s",id,secret))
3633

3734
user,err:=db.InsertUser(r.Context(), database.InsertUserParams{
3835
ID:uuid.New(),

‎coderd/httpmw/workspaceagent.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ func WorkspaceAgent(r *http.Request) database.WorkspaceAgent {
2929
funcExtractWorkspaceAgent(db database.Store)func(http.Handler) http.Handler {
3030
returnfunc(next http.Handler) http.Handler {
3131
returnhttp.HandlerFunc(func(rw http.ResponseWriter,r*http.Request) {
32-
cookie,err:=r.Cookie(codersdk.SessionTokenKey)
33-
iferr!=nil {
32+
cookieValue:=apiTokenFromRequest(r)
33+
ifcookieValue=="" {
3434
httpapi.Write(rw,http.StatusUnauthorized, codersdk.Response{
3535
Message:fmt.Sprintf("Cookie %q must be provided.",codersdk.SessionTokenKey),
3636
})
3737
return
3838
}
39-
token,err:=uuid.Parse(cookie.Value)
39+
token,err:=uuid.Parse(cookieValue)
4040
iferr!=nil {
4141
httpapi.Write(rw,http.StatusUnauthorized, codersdk.Response{
4242
Message:"Agent token is invalid.",

‎coderd/httpmw/workspaceagent_test.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,7 @@ func TestWorkspaceAgent(t *testing.T) {
2222
setup:=func(db database.Store) (*http.Request, uuid.UUID) {
2323
token:=uuid.New()
2424
r:=httptest.NewRequest("GET","/",nil)
25-
r.AddCookie(&http.Cookie{
26-
Name:codersdk.SessionTokenKey,
27-
Value:token.String(),
28-
})
25+
r.Header.Set(codersdk.SessionCustomHeader,token.String())
2926
returnr,token
3027
}
3128

‎coderd/httpmw/workspaceagentparam_test.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,7 @@ func TestWorkspaceAgentParam(t *testing.T) {
2929
hashed=sha256.Sum256([]byte(secret))
3030
)
3131
r:=httptest.NewRequest("GET","/",nil)
32-
r.AddCookie(&http.Cookie{
33-
Name:codersdk.SessionTokenKey,
34-
Value:fmt.Sprintf("%s-%s",id,secret),
35-
})
32+
r.Header.Set(codersdk.SessionCustomHeader,fmt.Sprintf("%s-%s",id,secret))
3633

3734
userID:=uuid.New()
3835
username,err:=cryptorand.String(8)

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp