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

feat: Implement (but not enforce) CSRF for FE requests#3786

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
Emyrk merged 29 commits intomainfromstevenmasley/csrf
Sep 13, 2022
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
29 commits
Select commitHold shift + click to select a range
52c7575
feat: Implement CSRF in cli client and FE api
EmyrkAug 31, 2022
642a29f
Make fmt
EmyrkAug 31, 2022
37c0ba8
const vs let
EmyrkAug 31, 2022
c774fcc
Fix lint error
presleypAug 31, 2022
3c75967
remove bad console log
EmyrkSep 1, 2022
ab48b4a
Add CSRF token in header
EmyrkSep 1, 2022
51d856e
Log error if token content is null
presleypSep 1, 2022
c8c8be0
Merge branch 'stevenmasley/csrf' of github.com:coder/coder into steve…
presleypSep 1, 2022
b03610b
Fix dev server csrf with hardcoded value
EmyrkSep 1, 2022
e798e11
Do not error log in JS unit test
EmyrkSep 1, 2022
1c4810a
Make fmt on js files
EmyrkSep 1, 2022
a6fdac8
Fix agent token checking
EmyrkSep 1, 2022
a343da9
Fix unit test
EmyrkSep 1, 2022
dd80cc9
Check auth cookie exists
EmyrkSep 1, 2022
08e76d4
Fix test auth
EmyrkSep 1, 2022
0aae08a
Fix logout test
EmyrkSep 1, 2022
3116964
Merge remote-tracking branch 'origin/main' into stevenmasley/csrf
EmyrkSep 13, 2022
10b4296
Fix merge issues
EmyrkSep 13, 2022
7177909
fixup! Fix merge issues
EmyrkSep 13, 2022
633118e
Make unit test use correct session value
EmyrkSep 13, 2022
5662a55
puppeteer does not have document defined
EmyrkSep 13, 2022
86b9ecf
Make fmt
EmyrkSep 13, 2022
ecaf61f
Update wireguard dep
EmyrkSep 13, 2022
484fe2b
Add comment about BE cookie
EmyrkSep 13, 2022
b18ea2e
chore: Ensure multiple version compatibility
EmyrkSep 13, 2022
b97225f
Merge remote-tracking branch 'origin/main' into stevenmasley/csrf
EmyrkSep 13, 2022
3f1eedf
Do not enforce CSRF
EmyrkSep 13, 2022
8f367d2
Add nolint
EmyrkSep 13, 2022
85dcbfd
Account for devurl cookie
EmyrkSep 13, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletionscoderd/coderd.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -187,6 +187,7 @@ func New(options *Options) *API {
next.ServeHTTP(w, r)
})
},
httpmw.CSRF(options.SecureAuthCookie),
)

apps := func(r chi.Router) {
Expand Down
6 changes: 3 additions & 3 deletionscoderd/httpapi/cookie_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,13 +17,13 @@ func TestStripCoderCookies(t *testing.T) {
"testing=hello; wow=test",
"testing=hello; wow=test",
}, {
"session_token=moo; wow=test",
"coder_session_token=moo; wow=test",
"wow=test",
}, {
"another_token=wow;session_token=ok",
"another_token=wow;coder_session_token=ok",
"another_token=wow",
}, {
"session_token=ok; oauth_state=wow; oauth_redirect=/",
"coder_session_token=ok; oauth_state=wow; oauth_redirect=/",
"",
}} {
tc := tc
Expand Down
41 changes: 34 additions & 7 deletionscoderd/httpmw/apikey.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -123,13 +123,7 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool
httpapi.Write(rw, code, response)
}

var cookieValue string
cookie, err := r.Cookie(codersdk.SessionTokenKey)
if err != nil {
cookieValue = r.URL.Query().Get(codersdk.SessionTokenKey)
} else {
cookieValue = cookie.Value
}
cookieValue := apiTokenFromRequest(r)
if cookieValue == "" {
write(http.StatusUnauthorized, codersdk.Response{
Message: signedOutErrorMessage,
Expand DownExpand Up@@ -335,3 +329,36 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool
})
}
}

// apiTokenFromRequest returns the api token from the request.
// Find the session token from:
// 1: The cookie
// 2: The old cookie
// 3. The coder_session_token query parameter
// 4. The custom auth header
func apiTokenFromRequest(r *http.Request) string {
cookie, err := r.Cookie(codersdk.SessionTokenKey)
if err == nil && cookie.Value != "" {
return cookie.Value
}

// TODO: @emyrk in October 2022, remove this oldCookie check.
//This is just to support the old cli for 1 release. Then everyone
//must update.
oldCookie, err := r.Cookie("session_token")
if err == nil && oldCookie.Value != "" {
return oldCookie.Value
}

urlValue := r.URL.Query().Get(codersdk.SessionTokenKey)
if urlValue != "" {
return urlValue
}

headerValue := r.Header.Get(codersdk.SessionCustomHeader)
if headerValue != "" {
return headerValue
}

return ""
}
60 changes: 12 additions & 48 deletionscoderd/httpmw/apikey_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -74,10 +74,7 @@ func TestAPIKey(t *testing.T) {
r = httptest.NewRequest("GET", "/", nil)
rw = httptest.NewRecorder()
)
r.AddCookie(&http.Cookie{
Name: codersdk.SessionTokenKey,
Value: "test-wow-hello",
})
r.Header.Set(codersdk.SessionCustomHeader, "test-wow-hello")

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

httpmw.ExtractAPIKey(db, nil, false)(successHandler).ServeHTTP(rw, r)
res := rw.Result()
Expand All@@ -110,10 +104,7 @@ func TestAPIKey(t *testing.T) {
r = httptest.NewRequest("GET", "/", nil)
rw = httptest.NewRecorder()
)
r.AddCookie(&http.Cookie{
Name: codersdk.SessionTokenKey,
Value: "testtestid-wow",
})
r.Header.Set(codersdk.SessionCustomHeader, "testtestid-wow")

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

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

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

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

sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
ID: id,
Expand DownExpand Up@@ -280,10 +259,7 @@ func TestAPIKey(t *testing.T) {
rw = httptest.NewRecorder()
user = createUser(r.Context(), t, db)
)
r.AddCookie(&http.Cookie{
Name: codersdk.SessionTokenKey,
Value: fmt.Sprintf("%s-%s", id, secret),
})
r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret))

sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
ID: id,
Expand DownExpand Up@@ -316,10 +292,7 @@ func TestAPIKey(t *testing.T) {
rw = httptest.NewRecorder()
user = createUser(r.Context(), t, db)
)
r.AddCookie(&http.Cookie{
Name: codersdk.SessionTokenKey,
Value: fmt.Sprintf("%s-%s", id, secret),
})
r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret))

sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
ID: id,
Expand DownExpand Up@@ -352,10 +325,7 @@ func TestAPIKey(t *testing.T) {
rw = httptest.NewRecorder()
user = createUser(r.Context(), t, db)
)
r.AddCookie(&http.Cookie{
Name: codersdk.SessionTokenKey,
Value: fmt.Sprintf("%s-%s", id, secret),
})
r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret))

sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
ID: id,
Expand DownExpand Up@@ -395,10 +365,7 @@ func TestAPIKey(t *testing.T) {
rw = httptest.NewRecorder()
user = createUser(r.Context(), t, db)
)
r.AddCookie(&http.Cookie{
Name: codersdk.SessionTokenKey,
Value: fmt.Sprintf("%s-%s", id, secret),
})
r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret))

sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
ID: id,
Expand DownExpand Up@@ -449,10 +416,7 @@ func TestAPIKey(t *testing.T) {
user = createUser(r.Context(), t, db)
)
r.RemoteAddr = "1.1.1.1:3555"
r.AddCookie(&http.Cookie{
Name: codersdk.SessionTokenKey,
Value: fmt.Sprintf("%s-%s", id, secret),
})
r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret))

_, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
ID: id,
Expand Down
5 changes: 1 addition & 4 deletionscoderd/httpmw/authorize_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -93,10 +93,7 @@ func TestExtractUserRoles(t *testing.T) {
})

req := httptest.NewRequest("GET", "/", nil)
req.AddCookie(&http.Cookie{
Name: codersdk.SessionTokenKey,
Value: token,
})
req.Header.Set(codersdk.SessionCustomHeader, token)

rtr.ServeHTTP(rw, req)
resp := rw.Result()
Expand Down
72 changes: 72 additions & 0 deletionscoderd/httpmw/csrf.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
package httpmw

import (
"net/http"
"regexp"

"github.com/justinas/nosurf"
"golang.org/x/xerrors"

"github.com/coder/coder/codersdk"
)

// CSRF is a middleware that verifies that a CSRF token is present in the request
// for non-GET requests.
func CSRF(secureCookie bool) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
mw := nosurf.New(next)
mw.SetBaseCookie(http.Cookie{Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, Secure: secureCookie})
mw.SetFailureHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Something is wrong with your CSRF token. Please refresh the page. If this error persists, try clearing your cookies.", http.StatusBadRequest)
}))

// Exempt all requests that do not require CSRF protection.
// All GET requests are exempt by default.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Is this not an issue? Can't we still send data with a GET?

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

CSRF really only needs to protect endpoints that have side effects. Our router checks the method on all API calls. Seehttps://security.stackexchange.com/questions/115794/should-i-use-csrf-protection-for-get-requests.

If we design our api correctly, there is no need to enforce CSRF on GET requests.

Kira-Pilot reacted with thumbs up emoji
mw.ExemptPath("/api/v2/csp/reports")

// Top level agent routes.
mw.ExemptRegexp(regexp.MustCompile("api/v2/workspaceagents/[^/]*$"))
// Agent authenticated routes
mw.ExemptRegexp(regexp.MustCompile("api/v2/workspaceagents/me/*"))
Comment on lines +23 to +30
Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Pretty sure this is all we need to exempt. If provisionerd needs to make api calls, we should omit that as well.

// Derp routes
mw.ExemptRegexp(regexp.MustCompile("derp/*"))

mw.ExemptFunc(func(r *http.Request) bool {
// Enable CSRF in November 2022 by deleting this "return true" line.
// CSRF is not enforced to ensure backwards compatibility with older
// cli versions.
//nolint:revive
return true

// CSRF only affects requests that automatically attach credentials via a cookie.
// If no cookie is present, then there is no risk of CSRF.
//nolint:govet
sessCookie, err := r.Cookie(codersdk.SessionTokenKey)
if xerrors.Is(err, http.ErrNoCookie) {
return true
}

if token := r.Header.Get(codersdk.SessionCustomHeader); token == sessCookie.Value {
// If the cookie and header match, we can assume this is the same as just using the
// custom header auth. Custom header auth can bypass CSRF, as CSRF attacks
// cannot add custom headers.
return true
}

if token := r.URL.Query().Get(codersdk.SessionTokenKey); token == sessCookie.Value {
// If the auth is set in a url param and matches the cookie, it
// is the same as just using the url param.
return true
}

// If the X-CSRF-TOKEN header is set, we can exempt the func if it's valid.
// This is the CSRF check.
sent := r.Header.Get("X-CSRF-TOKEN")
if sent != "" {
return nosurf.VerifyToken(nosurf.Token(r), sent)
}
return false
})
return mw
}
}
5 changes: 1 addition & 4 deletionscoderd/httpmw/organizationparam_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,10 +29,7 @@ func TestOrganizationParam(t *testing.T) {
r = httptest.NewRequest("GET", "/", nil)
hashed = sha256.Sum256([]byte(secret))
)
r.AddCookie(&http.Cookie{
Name: codersdk.SessionTokenKey,
Value: fmt.Sprintf("%s-%s", id, secret),
})
r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret))

userID := uuid.New()
username, err := cryptorand.String(8)
Expand Down
5 changes: 1 addition & 4 deletionscoderd/httpmw/templateparam_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,10 +29,7 @@ func TestTemplateParam(t *testing.T) {
hashed = sha256.Sum256([]byte(secret))
)
r := httptest.NewRequest("GET", "/", nil)
r.AddCookie(&http.Cookie{
Name: codersdk.SessionTokenKey,
Value: fmt.Sprintf("%s-%s", id, secret),
})
r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret))

userID := uuid.New()
username, err := cryptorand.String(8)
Expand Down
5 changes: 1 addition & 4 deletionscoderd/httpmw/templateversionparam_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,10 +29,7 @@ func TestTemplateVersionParam(t *testing.T) {
hashed = sha256.Sum256([]byte(secret))
)
r := httptest.NewRequest("GET", "/", nil)
r.AddCookie(&http.Cookie{
Name: codersdk.SessionTokenKey,
Value: fmt.Sprintf("%s-%s", id, secret),
})
r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret))

userID := uuid.New()
username, err := cryptorand.String(8)
Expand Down
5 changes: 1 addition & 4 deletionscoderd/httpmw/userparam_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,10 +29,7 @@ func TestUserParam(t *testing.T) {
r = httptest.NewRequest("GET", "/", nil)
rw = httptest.NewRecorder()
)
r.AddCookie(&http.Cookie{
Name: codersdk.SessionTokenKey,
Value: fmt.Sprintf("%s-%s", id, secret),
})
r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret))

user, err := db.InsertUser(r.Context(), database.InsertUserParams{
ID: uuid.New(),
Expand Down
6 changes: 3 additions & 3 deletionscoderd/httpmw/workspaceagent.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,14 +29,14 @@ func WorkspaceAgent(r *http.Request) database.WorkspaceAgent {
func ExtractWorkspaceAgent(db database.Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
cookie, err:=r.Cookie(codersdk.SessionTokenKey)
iferr != nil {
cookieValue:=apiTokenFromRequest(r)
ifcookieValue == "" {
httpapi.Write(rw, http.StatusUnauthorized, codersdk.Response{
Message: fmt.Sprintf("Cookie %q must be provided.", codersdk.SessionTokenKey),
})
return
}
token, err := uuid.Parse(cookie.Value)
token, err := uuid.Parse(cookieValue)
if err != nil {
httpapi.Write(rw, http.StatusUnauthorized, codersdk.Response{
Message: "Agent token is invalid.",
Expand Down
5 changes: 1 addition & 4 deletionscoderd/httpmw/workspaceagent_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,10 +22,7 @@ func TestWorkspaceAgent(t *testing.T) {
setup := func(db database.Store) (*http.Request, uuid.UUID) {
token := uuid.New()
r := httptest.NewRequest("GET", "/", nil)
r.AddCookie(&http.Cookie{
Name: codersdk.SessionTokenKey,
Value: token.String(),
})
r.Header.Set(codersdk.SessionCustomHeader, token.String())
return r, token
}

Expand Down
5 changes: 1 addition & 4 deletionscoderd/httpmw/workspaceagentparam_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,10 +29,7 @@ func TestWorkspaceAgentParam(t *testing.T) {
hashed = sha256.Sum256([]byte(secret))
)
r := httptest.NewRequest("GET", "/", nil)
r.AddCookie(&http.Cookie{
Name: codersdk.SessionTokenKey,
Value: fmt.Sprintf("%s-%s", id, secret),
})
r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret))

userID := uuid.New()
username, err := cryptorand.String(8)
Expand Down
Loading

[8]ページ先頭

©2009-2025 Movatter.jp