-
Notifications
You must be signed in to change notification settings - Fork 899
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 our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
52c7575
642a29f
37c0ba8
c774fcc
3c75967
ab48b4a
51d856e
c8c8be0
b03610b
e798e11
1c4810a
a6fdac8
a343da9
dd80cc9
08e76d4
0aae08a
3116964
10b4296
7177909
633118e
5662a55
86b9ecf
ecaf61f
484fe2b
b18ea2e
b97225f
3f1eedf
8f367d2
85dcbfd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
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. | ||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
|
||
mw.ExemptFunc(func(r *http.Request) bool { | ||
// CSRF only affects requests that automatically attach credentials via a cookie. | ||
// If no cookie is present, then there is no risk of CSRF. | ||
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 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,9 +20,11 @@ import ( | |
// Be sure to strip additional cookies in httpapi.StripCoder Cookies! | ||
const ( | ||
// SessionTokenKey represents the name of the cookie or query parameter the API key is stored in. | ||
SessionTokenKey = "session_token" | ||
OAuth2StateKey = "oauth_state" | ||
OAuth2RedirectKey = "oauth_redirect" | ||
SessionTokenKey = "coder_session_token" | ||
// SessionCustomHeader is the custom header to use for authentication. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is a custom header? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The cli needs to bypass CSRF (it'd be dumb to use on the cli), so we use a custom header rather than a cookie. The old code required using a In future we might want to use an Authorization Bearer token to be more standard? |
||
SessionCustomHeader = "Coder-Session-Token" | ||
OAuth2StateKey = "oauth_state" | ||
OAuth2RedirectKey = "oauth_redirect" | ||
) | ||
|
||
// New creates a Coder client for the provided URL. | ||
|
@@ -70,10 +72,7 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac | |
if err != nil { | ||
return nil, xerrors.Errorf("create request: %w", err) | ||
} | ||
req.AddCookie(&http.Cookie{ | ||
Name: SessionTokenKey, | ||
Value: c.SessionToken, | ||
}) | ||
req.Header.Set(SessionCustomHeader, c.SessionToken) | ||
if body != nil { | ||
req.Header.Set("Content-Type", "application/json") | ||
} | ||
|
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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. See https://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.