From 52c757531a2602765bd0d5c19383322e29095a1d Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 31 Aug 2022 17:09:45 -0400 Subject: [PATCH 01/26] feat: Implement CSRF in cli client and FE api --- coderd/coderd.go | 1 + coderd/httpmw/apikey.go | 19 ++++++- coderd/httpmw/apikey_test.go | 60 +++++---------------- coderd/httpmw/authorize_test.go | 5 +- coderd/httpmw/csrf.go | 63 ++++++++++++++++++++++ coderd/httpmw/organizationparam_test.go | 5 +- coderd/httpmw/templateparam_test.go | 5 +- coderd/httpmw/templateversionparam_test.go | 5 +- coderd/httpmw/userparam_test.go | 5 +- coderd/httpmw/workspaceagent_test.go | 5 +- coderd/httpmw/workspaceagentparam_test.go | 5 +- coderd/httpmw/workspacebuildparam_test.go | 5 +- coderd/httpmw/workspaceparam_test.go | 10 +--- codersdk/client.go | 13 +++-- site/src/api/api.ts | 9 ++++ 15 files changed, 119 insertions(+), 96 deletions(-) create mode 100644 coderd/httpmw/csrf.go diff --git a/coderd/coderd.go b/coderd/coderd.go index 58a4386c90005..052584917a926 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -130,6 +130,7 @@ func New(options *Options) *API { httpmw.Recover(api.Logger), httpmw.Logger(api.Logger), httpmw.Prometheus(options.PrometheusRegistry), + httpmw.CSRF(options.SecureAuthCookie), ) apps := func(r chi.Router) { diff --git a/coderd/httpmw/apikey.go b/coderd/httpmw/apikey.go index ed2a1e617f567..70d4ceeec1971 100644 --- a/coderd/httpmw/apikey.go +++ b/coderd/httpmw/apikey.go @@ -82,12 +82,29 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool } var cookieValue string + // Find the session token from: + // 1: The cookie + // 2: The old cookie + // 3. The coder_session_token query parameter + // 4. The custom auth header cookie, err := r.Cookie(codersdk.SessionTokenKey) if err != nil { - cookieValue = r.URL.Query().Get(codersdk.SessionTokenKey) + // 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 { + cookieValue = r.URL.Query().Get(codersdk.SessionTokenKey) + if cookieValue == "" { + cookieValue = r.Header.Get(codersdk.SessionCustomHeader) + } + } else { + cookieValue = oldCookie.Value + } } else { cookieValue = cookie.Value } + if cookieValue == "" { write(http.StatusUnauthorized, codersdk.Response{ Message: signedOutErrorMessage, diff --git a/coderd/httpmw/apikey_test.go b/coderd/httpmw/apikey_test.go index adc13b2f176fa..fc7bf92af781b 100644 --- a/coderd/httpmw/apikey_test.go +++ b/coderd/httpmw/apikey_test.go @@ -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() @@ -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() @@ -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() @@ -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() @@ -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")) @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, diff --git a/coderd/httpmw/authorize_test.go b/coderd/httpmw/authorize_test.go index dcae5bff96526..32e2742ccd47f 100644 --- a/coderd/httpmw/authorize_test.go +++ b/coderd/httpmw/authorize_test.go @@ -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() diff --git a/coderd/httpmw/csrf.go b/coderd/httpmw/csrf.go new file mode 100644 index 0000000000000..e5f27bdae729e --- /dev/null +++ b/coderd/httpmw/csrf.go @@ -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/*")) + + 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 + } +} diff --git a/coderd/httpmw/organizationparam_test.go b/coderd/httpmw/organizationparam_test.go index bdf442391091c..0da000802879f 100644 --- a/coderd/httpmw/organizationparam_test.go +++ b/coderd/httpmw/organizationparam_test.go @@ -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) diff --git a/coderd/httpmw/templateparam_test.go b/coderd/httpmw/templateparam_test.go index 7da35889aa536..1e936b403ee5a 100644 --- a/coderd/httpmw/templateparam_test.go +++ b/coderd/httpmw/templateparam_test.go @@ -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) diff --git a/coderd/httpmw/templateversionparam_test.go b/coderd/httpmw/templateversionparam_test.go index fe4ebba9dfcc3..6c28cb743f6ff 100644 --- a/coderd/httpmw/templateversionparam_test.go +++ b/coderd/httpmw/templateversionparam_test.go @@ -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) diff --git a/coderd/httpmw/userparam_test.go b/coderd/httpmw/userparam_test.go index d21f2a583d58f..0b5e6013c38d2 100644 --- a/coderd/httpmw/userparam_test.go +++ b/coderd/httpmw/userparam_test.go @@ -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(), diff --git a/coderd/httpmw/workspaceagent_test.go b/coderd/httpmw/workspaceagent_test.go index eef7fc80847f1..a33bc51bdce9e 100644 --- a/coderd/httpmw/workspaceagent_test.go +++ b/coderd/httpmw/workspaceagent_test.go @@ -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 } diff --git a/coderd/httpmw/workspaceagentparam_test.go b/coderd/httpmw/workspaceagentparam_test.go index c2d047fdda983..435308bd3d4c9 100644 --- a/coderd/httpmw/workspaceagentparam_test.go +++ b/coderd/httpmw/workspaceagentparam_test.go @@ -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) diff --git a/coderd/httpmw/workspacebuildparam_test.go b/coderd/httpmw/workspacebuildparam_test.go index c993a0cb66fb6..271229067ecb7 100644 --- a/coderd/httpmw/workspacebuildparam_test.go +++ b/coderd/httpmw/workspacebuildparam_test.go @@ -29,10 +29,7 @@ func TestWorkspaceBuildParam(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) diff --git a/coderd/httpmw/workspaceparam_test.go b/coderd/httpmw/workspaceparam_test.go index 5b3f9a5830bc7..7dfaa55724996 100644 --- a/coderd/httpmw/workspaceparam_test.go +++ b/coderd/httpmw/workspaceparam_test.go @@ -32,10 +32,7 @@ func TestWorkspaceParam(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) @@ -340,10 +337,7 @@ func setupWorkspaceWithAgents(t testing.TB, cfg setupConfig) (database.Store, *h 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) diff --git a/codersdk/client.go b/codersdk/client.go index bbeaa990da81b..b464c6c0f3f7e 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -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. + 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") } diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 20a5156e9d3f9..415a805fa3ea9 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -4,6 +4,15 @@ import * as Types from "./types" import { WorkspaceBuildTransition } from "./types" import * as TypesGen from "./typesGenerated" + +// Always attach CSRF token to all requests +let token = document.head.querySelector('meta[property="csrf-token"]'); +if (token) { + axios.defaults.headers.common['X-CSRF-TOKEN'] = token.getAttribute("content")!; +} else { + console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token'); +} + const CONTENT_TYPE_JSON: AxiosRequestHeaders = { "Content-Type": "application/json", } From 642a29f0b154e66dab4c926bb8881cb5b31cba26 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 31 Aug 2022 17:17:38 -0400 Subject: [PATCH 02/26] Make fmt --- site/src/api/api.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 415a805fa3ea9..4313f19fb23ab 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -4,13 +4,12 @@ import * as Types from "./types" import { WorkspaceBuildTransition } from "./types" import * as TypesGen from "./typesGenerated" - // Always attach CSRF token to all requests -let token = document.head.querySelector('meta[property="csrf-token"]'); +let token = document.head.querySelector('meta[property="csrf-token"]') if (token) { - axios.defaults.headers.common['X-CSRF-TOKEN'] = token.getAttribute("content")!; + axios.defaults.headers.common["X-CSRF-TOKEN"] = token.getAttribute("content")! } else { - console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token'); + console.error("CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token") } const CONTENT_TYPE_JSON: AxiosRequestHeaders = { From 37c0ba8ec77ec8b63ad282b724b19184271b9557 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 31 Aug 2022 17:22:57 -0400 Subject: [PATCH 03/26] const vs let --- site/src/api/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 4313f19fb23ab..9ab44d7f0faee 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -5,7 +5,7 @@ import { WorkspaceBuildTransition } from "./types" import * as TypesGen from "./typesGenerated" // Always attach CSRF token to all requests -let token = document.head.querySelector('meta[property="csrf-token"]') +const token = document.head.querySelector('meta[property="csrf-token"]') if (token) { axios.defaults.headers.common["X-CSRF-TOKEN"] = token.getAttribute("content")! } else { From c774fcc60fcd1bc10a7d0903189f6bfb9444720b Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Wed, 31 Aug 2022 22:08:33 +0000 Subject: [PATCH 04/26] Fix lint error --- site/src/api/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 9ab44d7f0faee..b786627bfd9ad 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -7,7 +7,7 @@ import * as TypesGen from "./typesGenerated" // Always attach CSRF token to all requests const token = document.head.querySelector('meta[property="csrf-token"]') if (token) { - axios.defaults.headers.common["X-CSRF-TOKEN"] = token.getAttribute("content")! + axios.defaults.headers.common["X-CSRF-TOKEN"] = token.getAttribute("content") ?? "" } else { console.error("CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token") } From 3c7596726316a82ce9681783c917f06cdd4ced58 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 31 Aug 2022 20:32:09 -0400 Subject: [PATCH 05/26] remove bad console log --- site/src/api/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index b786627bfd9ad..a756f1b4bc178 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -9,7 +9,7 @@ const token = document.head.querySelector('meta[property="csrf-token"]') if (token) { axios.defaults.headers.common["X-CSRF-TOKEN"] = token.getAttribute("content") ?? "" } else { - console.error("CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token") + console.error("CSRF token not found") } const CONTENT_TYPE_JSON: AxiosRequestHeaders = { From ab48b4a2147b154676f97dd774db03e7082e8aae Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 1 Sep 2022 09:08:17 -0400 Subject: [PATCH 06/26] Add CSRF token in header --- site/webpack.dev.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/site/webpack.dev.ts b/site/webpack.dev.ts index cb43015edd7df..c58b4a3e43bcd 100644 --- a/site/webpack.dev.ts +++ b/site/webpack.dev.ts @@ -51,6 +51,7 @@ const config: Configuration = { }, devMiddleware: { publicPath: "/", + headers: {"Set-Cookie":"csrf_token=JXm9hOUdZctWt0ZZGAy9xiS/gxMKYOThdxjjMnMUyn4=; Path=/; HttpOnly; SameSite=Lax"} }, headers: { "Access-Control-Allow-Origin": "*", From 51d856e1dda550b80ab1f8057312c896477738ff Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Thu, 1 Sep 2022 13:43:33 +0000 Subject: [PATCH 07/26] Log error if token content is null --- site/src/api/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index b786627bfd9ad..bfa5b0735cc4e 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -6,7 +6,7 @@ import * as TypesGen from "./typesGenerated" // Always attach CSRF token to all requests const token = document.head.querySelector('meta[property="csrf-token"]') -if (token) { +if (token !== null && token.getAttribute("content") !== null) { axios.defaults.headers.common["X-CSRF-TOKEN"] = token.getAttribute("content") ?? "" } else { console.error("CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token") From b03610b691ea099b542034baaca5927b375360f6 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 1 Sep 2022 10:15:07 -0400 Subject: [PATCH 08/26] Fix dev server csrf with hardcoded value --- site/src/api/api.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 6ac04fae68269..57c571b374af3 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -4,10 +4,27 @@ import * as Types from "./types" import { WorkspaceBuildTransition } from "./types" import * as TypesGen from "./typesGenerated" +export const hardCodedCSRFCookie = (): string => { + // This is a hard coded CSRF token/cookie pair for local development. + // In prod, the GoLang webserver generates a random cookie with a new token for + // each document request. For local development, we don't use the Go webserver for static files, + // so this is the 'hack' to make local development work with remote apis. + // The CSRF cookie for this token is "JXm9hOUdZctWt0ZZGAy9xiS/gxMKYOThdxjjMnMUyn4=" + const csrfToken = "KNKvagCBEHZK7ihe2t7fj6VeJ0UyTDco1yVUJE8N06oNqxLu5Zx1vRxZbgfC0mJJgeGkVjgs08mgPbcWPBkZ1A==" + axios.defaults.headers.common["X-CSRF-TOKEN"] = csrfToken + return csrfToken +} + // Always attach CSRF token to all requests const token = document.head.querySelector('meta[property="csrf-token"]') if (token !== null && token.getAttribute("content") !== null) { - axios.defaults.headers.common["X-CSRF-TOKEN"] = token.getAttribute("content") ?? "" + if(process.env.NODE_ENV === "development") { + // Development mode uses a hard-coded CSRF token + axios.defaults.headers.common["X-CSRF-TOKEN"] = hardCodedCSRFCookie() + token.setAttribute("content", hardCodedCSRFCookie()) + } else { + axios.defaults.headers.common["X-CSRF-TOKEN"] = token.getAttribute("content") ?? "" + } } else { console.error("CSRF token not found") } From e798e114a02f0ee8746ade8e7349cc19f5244d52 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 1 Sep 2022 10:21:29 -0400 Subject: [PATCH 09/26] Do not error log in JS unit test --- site/src/api/api.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 57c571b374af3..61743fe074e04 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -26,7 +26,10 @@ if (token !== null && token.getAttribute("content") !== null) { axios.defaults.headers.common["X-CSRF-TOKEN"] = token.getAttribute("content") ?? "" } } else { - console.error("CSRF token not found") + // Do not write error logs if we are in a FE unit test. + if(process.env.JEST_WORKER_ID === undefined) { + console.error("CSRF token not found") + } } const CONTENT_TYPE_JSON: AxiosRequestHeaders = { From 1c4810ad65935b02bccf23a358d0a4c2596c8c03 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 1 Sep 2022 10:22:37 -0400 Subject: [PATCH 10/26] Make fmt on js files --- site/src/api/api.ts | 7 ++++--- site/webpack.dev.ts | 5 ++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 61743fe074e04..de2369d742be9 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -10,7 +10,8 @@ export const hardCodedCSRFCookie = (): string => { // each document request. For local development, we don't use the Go webserver for static files, // so this is the 'hack' to make local development work with remote apis. // The CSRF cookie for this token is "JXm9hOUdZctWt0ZZGAy9xiS/gxMKYOThdxjjMnMUyn4=" - const csrfToken = "KNKvagCBEHZK7ihe2t7fj6VeJ0UyTDco1yVUJE8N06oNqxLu5Zx1vRxZbgfC0mJJgeGkVjgs08mgPbcWPBkZ1A==" + const csrfToken = + "KNKvagCBEHZK7ihe2t7fj6VeJ0UyTDco1yVUJE8N06oNqxLu5Zx1vRxZbgfC0mJJgeGkVjgs08mgPbcWPBkZ1A==" axios.defaults.headers.common["X-CSRF-TOKEN"] = csrfToken return csrfToken } @@ -18,7 +19,7 @@ export const hardCodedCSRFCookie = (): string => { // Always attach CSRF token to all requests const token = document.head.querySelector('meta[property="csrf-token"]') if (token !== null && token.getAttribute("content") !== null) { - if(process.env.NODE_ENV === "development") { + if (process.env.NODE_ENV === "development") { // Development mode uses a hard-coded CSRF token axios.defaults.headers.common["X-CSRF-TOKEN"] = hardCodedCSRFCookie() token.setAttribute("content", hardCodedCSRFCookie()) @@ -27,7 +28,7 @@ if (token !== null && token.getAttribute("content") !== null) { } } else { // Do not write error logs if we are in a FE unit test. - if(process.env.JEST_WORKER_ID === undefined) { + if (process.env.JEST_WORKER_ID === undefined) { console.error("CSRF token not found") } } diff --git a/site/webpack.dev.ts b/site/webpack.dev.ts index c58b4a3e43bcd..09f03fef28dab 100644 --- a/site/webpack.dev.ts +++ b/site/webpack.dev.ts @@ -51,7 +51,10 @@ const config: Configuration = { }, devMiddleware: { publicPath: "/", - headers: {"Set-Cookie":"csrf_token=JXm9hOUdZctWt0ZZGAy9xiS/gxMKYOThdxjjMnMUyn4=; Path=/; HttpOnly; SameSite=Lax"} + headers: { + "Set-Cookie": + "csrf_token=JXm9hOUdZctWt0ZZGAy9xiS/gxMKYOThdxjjMnMUyn4=; Path=/; HttpOnly; SameSite=Lax", + }, }, headers: { "Access-Control-Allow-Origin": "*", From a6fdac86fbe3818c3305ccc4bf61efd87df46830 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 1 Sep 2022 10:41:58 -0400 Subject: [PATCH 11/26] Fix agent token checking --- coderd/httpapi/cookie_test.go | 6 ++-- coderd/httpmw/apikey.go | 58 +++++++++++++++++++-------------- coderd/httpmw/workspaceagent.go | 6 ++-- 3 files changed, 40 insertions(+), 30 deletions(-) diff --git a/coderd/httpapi/cookie_test.go b/coderd/httpapi/cookie_test.go index 546f1d2a7b30c..48c66abc439f0 100644 --- a/coderd/httpapi/cookie_test.go +++ b/coderd/httpapi/cookie_test.go @@ -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 diff --git a/coderd/httpmw/apikey.go b/coderd/httpmw/apikey.go index 70d4ceeec1971..6927c56a8068c 100644 --- a/coderd/httpmw/apikey.go +++ b/coderd/httpmw/apikey.go @@ -81,30 +81,7 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool httpapi.Write(rw, code, response) } - var cookieValue string - // Find the session token from: - // 1: The cookie - // 2: The old cookie - // 3. The coder_session_token query parameter - // 4. The custom auth header - cookie, err := r.Cookie(codersdk.SessionTokenKey) - if err != nil { - // 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 { - cookieValue = r.URL.Query().Get(codersdk.SessionTokenKey) - if cookieValue == "" { - cookieValue = r.Header.Get(codersdk.SessionCustomHeader) - } - } else { - cookieValue = oldCookie.Value - } - } else { - cookieValue = cookie.Value - } - + cookieValue := apiTokenFromRequest(r) if cookieValue == "" { write(http.StatusUnauthorized, codersdk.Response{ Message: signedOutErrorMessage, @@ -310,3 +287,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 "" +} diff --git a/coderd/httpmw/workspaceagent.go b/coderd/httpmw/workspaceagent.go index 70331d80a540f..26a7eae41875c 100644 --- a/coderd/httpmw/workspaceagent.go +++ b/coderd/httpmw/workspaceagent.go @@ -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) - if err != nil { + cookieValue := apiTokenFromRequest(r) + if cookieValue == "" { 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.", From a343da91b762d38dc388223685d3778d44a1335a Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 1 Sep 2022 10:51:15 -0400 Subject: [PATCH 12/26] Fix unit test --- coderd/userauth_test.go | 14 ++++++++++++-- coderd/users_test.go | 10 +++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/coderd/userauth_test.go b/coderd/userauth_test.go index 59f8c1d5e0c79..342d581a504d7 100644 --- a/coderd/userauth_test.go +++ b/coderd/userauth_test.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "net/url" + "strings" "testing" "time" @@ -244,7 +245,7 @@ func TestUserOAuth2Github(t *testing.T) { resp := oauth2Callback(t, client) require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) - client.SessionToken = resp.Cookies()[0].Value + client.SessionToken = authCookieValue(resp.Cookies()) user, err := client.User(context.Background(), "me") require.NoError(t, err) require.Equal(t, "kyle@coder.com", user.Email) @@ -374,7 +375,7 @@ func TestUserOIDC(t *testing.T) { defer cancel() if tc.Username != "" { - client.SessionToken = resp.Cookies()[0].Value + client.SessionToken = authCookieValue(resp.Cookies()) user, err := client.User(ctx, "me") require.NoError(t, err) require.Equal(t, tc.Username, user.Username) @@ -503,3 +504,12 @@ func oidcCallback(t *testing.T, client *codersdk.Client) *http.Response { func i64ptr(i int64) *int64 { return &i } + +func authCookieValue(cookies []*http.Cookie) string { + for _, cookie := range cookies { + if strings.Contains(cookie.Name, "token") { + return cookie.Value + } + } + return "" +} diff --git a/coderd/users_test.go b/coderd/users_test.go index 6836c257290d1..3622886b84191 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -281,10 +281,14 @@ func TestPostLogout(t *testing.T) { require.Equal(t, http.StatusOK, res.StatusCode) cookies := res.Cookies() - require.Len(t, cookies, 1, "Exactly one cookie should be returned") + require.Len(t, cookies, 2, "Exactly one cookie should be returned") - require.Equal(t, codersdk.SessionTokenKey, cookies[0].Name, "Cookie should be the auth cookie") - require.Equal(t, -1, cookies[0].MaxAge, "Cookie should be set to delete") + for _, cookie := range cookies { + if cookie.Name == codersdk.SessionTokenKey { + require.Equal(t, codersdk.SessionTokenKey, cookies[0].Name, "Cookie should be the auth cookie") + require.Equal(t, -1, cookies[0].MaxAge, "Cookie should be set to delete") + } + } _, err = client.GetAPIKey(ctx, admin.UserID.String(), keyID) sdkErr := &codersdk.Error{} From dd80cc9dbff7fdbafcc53314ef2d96b9141d1ad1 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 1 Sep 2022 10:54:31 -0400 Subject: [PATCH 13/26] Check auth cookie exists --- coderd/users_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/coderd/users_test.go b/coderd/users_test.go index 3622886b84191..b0b4d0156e028 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -283,12 +283,15 @@ func TestPostLogout(t *testing.T) { cookies := res.Cookies() require.Len(t, cookies, 2, "Exactly one cookie should be returned") + var found bool for _, cookie := range cookies { if cookie.Name == codersdk.SessionTokenKey { require.Equal(t, codersdk.SessionTokenKey, cookies[0].Name, "Cookie should be the auth cookie") require.Equal(t, -1, cookies[0].MaxAge, "Cookie should be set to delete") + found = true } } + require.True(t, found, "auth cookie should be returned") _, err = client.GetAPIKey(ctx, admin.UserID.String(), keyID) sdkErr := &codersdk.Error{} From 08e76d4c0e49e9bbe57e06332f548355143551a2 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 1 Sep 2022 11:44:26 -0400 Subject: [PATCH 14/26] Fix test auth --- coderd/userauth_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/coderd/userauth_test.go b/coderd/userauth_test.go index 342d581a504d7..e0c14e29edc9e 100644 --- a/coderd/userauth_test.go +++ b/coderd/userauth_test.go @@ -8,7 +8,6 @@ import ( "io" "net/http" "net/url" - "strings" "testing" "time" @@ -507,7 +506,7 @@ func i64ptr(i int64) *int64 { func authCookieValue(cookies []*http.Cookie) string { for _, cookie := range cookies { - if strings.Contains(cookie.Name, "token") { + if cookie.Name == codersdk.SessionTokenKey { return cookie.Value } } From 0aae08acce5b36342ee9e9ebceb32430b2b38a01 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 1 Sep 2022 14:55:15 -0400 Subject: [PATCH 15/26] Fix logout test --- coderd/users_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/users_test.go b/coderd/users_test.go index b0b4d0156e028..9ce495ee85ebd 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -286,8 +286,8 @@ func TestPostLogout(t *testing.T) { var found bool for _, cookie := range cookies { if cookie.Name == codersdk.SessionTokenKey { - require.Equal(t, codersdk.SessionTokenKey, cookies[0].Name, "Cookie should be the auth cookie") - require.Equal(t, -1, cookies[0].MaxAge, "Cookie should be set to delete") + require.Equal(t, codersdk.SessionTokenKey, cookie.Name, "Cookie should be the auth cookie") + require.Equal(t, -1, cookie.MaxAge, "Cookie should be set to delete") found = true } } From 10b42960c517393921cc925572b8edea8d830b39 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 13 Sep 2022 09:27:05 -0400 Subject: [PATCH 16/26] Fix merge issues --- coderd/coderd.go | 1 - coderd/httpmw/csrf.go | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index b8d99641b0a3f..87633709c05c7 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -406,7 +406,6 @@ func New(options *Options) *API { httpmw.ExtractWorkspaceParam(options.Database), ) r.Get("/", api.workspaceAgent) - r.Post("/peer", api.postWorkspaceAgentWireguardPeer) r.Get("/dial", api.workspaceAgentDial) r.Get("/turn", api.userWorkspaceAgentTurn) r.Get("/pty", api.workspaceAgentPTY) diff --git a/coderd/httpmw/csrf.go b/coderd/httpmw/csrf.go index e5f27bdae729e..e94c3a7cb17c9 100644 --- a/coderd/httpmw/csrf.go +++ b/coderd/httpmw/csrf.go @@ -16,7 +16,7 @@ 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) { + mw.SetFailureHandler(http.HandlerFunc(func(wg 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) })) @@ -28,6 +28,8 @@ func CSRF(secureCookie bool) func(next http.Handler) http.Handler { mw.ExemptRegexp(regexp.MustCompile("api/v2/workspaceagents/[^/]*$")) // Agent authenticated routes mw.ExemptRegexp(regexp.MustCompile("api/v2/workspaceagents/me/*")) + // Derp routes + mw.ExemptRegexp(regexp.MustCompile("derp/*")) mw.ExemptFunc(func(r *http.Request) bool { // CSRF only affects requests that automatically attach credentials via a cookie. From 71779090a6b6e1ce9798a6ef04d9d2120384e6d1 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 13 Sep 2022 09:27:43 -0400 Subject: [PATCH 17/26] fixup! Fix merge issues --- coderd/httpmw/csrf.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/httpmw/csrf.go b/coderd/httpmw/csrf.go index e94c3a7cb17c9..b06090cb405fd 100644 --- a/coderd/httpmw/csrf.go +++ b/coderd/httpmw/csrf.go @@ -16,7 +16,7 @@ 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(wg http.ResponseWriter, r *http.Request) { + 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) })) From 633118e5a8ed9c5806f717435fda505f84820d7c Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 13 Sep 2022 09:39:25 -0400 Subject: [PATCH 18/26] Make unit test use correct session value --- coderd/userauth_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/userauth_test.go b/coderd/userauth_test.go index 13f8f209722ba..5c411155904bb 100644 --- a/coderd/userauth_test.go +++ b/coderd/userauth_test.go @@ -405,7 +405,7 @@ func TestUserOIDC(t *testing.T) { } if tc.AvatarURL != "" { - client.SessionToken = resp.Cookies()[0].Value + client.SessionToken = authCookieValue(resp.Cookies()) user, err := client.User(ctx, "me") require.NoError(t, err) require.Equal(t, tc.AvatarURL, user.AvatarURL) From 5662a55fb67a50974fc2387c4a1cbaa6ec0fccaa Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 13 Sep 2022 09:53:20 -0400 Subject: [PATCH 19/26] puppeteer does not have document defined Might need to await some page load or detect in puppeteer --- site/src/api/api.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 3713e0d6a2c68..ee34bda72ede0 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -16,8 +16,13 @@ export const hardCodedCSRFCookie = (): string => { return csrfToken } -// Always attach CSRF token to all requests -const token = document.head.querySelector('meta[property="csrf-token"]') +// Always attach CSRF token to all requests. +// In puppeteer the document is undefined. In those cases, just +// do nothing. +const token = typeof document !== 'undefined' ? + document.head.querySelector('meta[property="csrf-token"]') : + null + if (token !== null && token.getAttribute("content") !== null) { if (process.env.NODE_ENV === "development") { // Development mode uses a hard-coded CSRF token From 86b9ecf2821b748c29fbadb164324a7fe104bdd4 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 13 Sep 2022 10:10:34 -0400 Subject: [PATCH 20/26] Make fmt --- site/src/api/api.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index ee34bda72ede0..3e2ea86c16d8b 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -19,9 +19,10 @@ export const hardCodedCSRFCookie = (): string => { // Always attach CSRF token to all requests. // In puppeteer the document is undefined. In those cases, just // do nothing. -const token = typeof document !== 'undefined' ? - document.head.querySelector('meta[property="csrf-token"]') : - null +const token = + typeof document !== "undefined" + ? document.head.querySelector('meta[property="csrf-token"]') + : null if (token !== null && token.getAttribute("content") !== null) { if (process.env.NODE_ENV === "development") { From ecaf61f4169f2adc046c210072b0264bcb725015 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 13 Sep 2022 15:47:17 +0000 Subject: [PATCH 21/26] Update wireguard dep --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 13832bcfd4cf2..23576413066d0 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,7 @@ replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20220912224234-e80c replace github.com/gliderlabs/ssh => github.com/coder/ssh v0.0.0-20220811105153-fcea99919338 // Fixes a deadlock on close in devtunnel. -replace golang.zx2c4.com/wireguard => github.com/coder/wireguard-go v0.0.0-20220913030355-902de6e9b175 +replace golang.zx2c4.com/wireguard => github.com/coder/wireguard-go v0.0.0-20220913030931-b1b3bb45caf9 require ( cdr.dev/slog v1.4.2-0.20220525200111-18dce5c2cd5f diff --git a/go.sum b/go.sum index e3d95afa7bd3a..41957b92a542a 100644 --- a/go.sum +++ b/go.sum @@ -357,8 +357,8 @@ github.com/coder/ssh v0.0.0-20220811105153-fcea99919338 h1:tN5GKFT68YLVzJoA8AHui github.com/coder/ssh v0.0.0-20220811105153-fcea99919338/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914= github.com/coder/tailscale v1.1.1-0.20220912224234-e80caec6c05f h1:NN9O1Pgno2QQy+JBnZk1VQ3vyAmWaB+yEotUDEuFKm8= github.com/coder/tailscale v1.1.1-0.20220912224234-e80caec6c05f/go.mod h1:5amxy08qijEa8bcTW2SeIy4MIqcmd7LMsuOxqOlj2Ak= -github.com/coder/wireguard-go v0.0.0-20220913030355-902de6e9b175 h1:obgyZIctZKcztM+TiIBUIJkOf04L9Fg+oLb47XEGy44= -github.com/coder/wireguard-go v0.0.0-20220913030355-902de6e9b175/go.mod h1:enML0deDxY1ux+B6ANGiwtg0yAJi1rctkTpcHNAVPyg= +github.com/coder/wireguard-go v0.0.0-20220913030931-b1b3bb45caf9 h1:AeU4w8hSB+XEj3e8HjvEUTy/MWQd6tddnr9dELrRjKk= +github.com/coder/wireguard-go v0.0.0-20220913030931-b1b3bb45caf9/go.mod h1:enML0deDxY1ux+B6ANGiwtg0yAJi1rctkTpcHNAVPyg= github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= From 484fe2b80b183ee6697f859024ce46c06de28bac Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 13 Sep 2022 12:06:16 -0400 Subject: [PATCH 22/26] Add comment about BE cookie --- site/webpack.dev.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/site/webpack.dev.ts b/site/webpack.dev.ts index 09f03fef28dab..9e59efa6aa3a9 100644 --- a/site/webpack.dev.ts +++ b/site/webpack.dev.ts @@ -52,6 +52,8 @@ const config: Configuration = { devMiddleware: { publicPath: "/", headers: { + // This header corresponds to "src/api/api.ts"'s hardcoded FE token. + // This is the secret side of the CSRF double cookie submit method. "Set-Cookie": "csrf_token=JXm9hOUdZctWt0ZZGAy9xiS/gxMKYOThdxjjMnMUyn4=; Path=/; HttpOnly; SameSite=Lax", }, From b18ea2e90e56c01eddf3c3856a5eeacb4247746c Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 13 Sep 2022 12:57:15 -0400 Subject: [PATCH 23/26] chore: Ensure multiple version compatibility --- coderd/coderd.go | 4 +++- codersdk/client.go | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 87633709c05c7..2f39d751dfdd8 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -167,7 +167,9 @@ func New(options *Options) *API { next.ServeHTTP(w, r) }) }, - httpmw.CSRF(options.SecureAuthCookie), + // Enable CSRF in November 2022 by uncommenting out this line. + // This is commented out for backwards compatibility. + // httpmw.CSRF(options.SecureAuthCookie), ) apps := func(r chi.Router) { diff --git a/codersdk/client.go b/codersdk/client.go index 08339e270f59b..6fa34e91b3e06 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -73,6 +73,14 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac return nil, xerrors.Errorf("create request: %w", err) } req.Header.Set(SessionCustomHeader, c.SessionToken) + + // Delete this custom cookie set in November 2022. This is just to remain + // backwards compatible with older versions of Coder. + req.AddCookie(&http.Cookie{ + Name: "session_token", + Value: c.SessionToken, + }) + if body != nil { req.Header.Set("Content-Type", "application/json") } From 3f1eedf5a47f0487680bf29bfdb1f7ce8d54f608 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 13 Sep 2022 14:52:46 -0400 Subject: [PATCH 24/26] Do not enforce CSRF --- coderd/coderd.go | 4 +--- coderd/httpmw/csrf.go | 5 +++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 6343e6a9ba654..4b5f143bdee9b 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -187,9 +187,7 @@ func New(options *Options) *API { next.ServeHTTP(w, r) }) }, - // Enable CSRF in November 2022 by uncommenting out this line. - // This is commented out for backwards compatibility. - // httpmw.CSRF(options.SecureAuthCookie), + httpmw.CSRF(options.SecureAuthCookie), ) apps := func(r chi.Router) { diff --git a/coderd/httpmw/csrf.go b/coderd/httpmw/csrf.go index b06090cb405fd..6bde413abe0f8 100644 --- a/coderd/httpmw/csrf.go +++ b/coderd/httpmw/csrf.go @@ -32,6 +32,11 @@ func CSRF(secureCookie bool) func(next http.Handler) http.Handler { 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. + 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. sessCookie, err := r.Cookie(codersdk.SessionTokenKey) From 8f367d2c5d7ababb1cb46c36fb2bf10fec5a2501 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 13 Sep 2022 15:00:41 -0400 Subject: [PATCH 25/26] Add nolint --- coderd/httpmw/csrf.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/coderd/httpmw/csrf.go b/coderd/httpmw/csrf.go index 6bde413abe0f8..b4f0880569187 100644 --- a/coderd/httpmw/csrf.go +++ b/coderd/httpmw/csrf.go @@ -35,10 +35,12 @@ func CSRF(secureCookie bool) func(next http.Handler) http.Handler { // 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 From 85dcbfde90d48696c00d7f8bc1837197229536eb Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 13 Sep 2022 15:06:58 -0400 Subject: [PATCH 26/26] Account for devurl cookie --- coderd/users_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/coderd/users_test.go b/coderd/users_test.go index c501b41daafec..6f1c2d7ebba37 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -330,7 +330,6 @@ func TestPostLogout(t *testing.T) { require.Equal(t, http.StatusOK, res.StatusCode) cookies := res.Cookies() - require.Len(t, cookies, 2, "Exactly two cookies should be returned") var found bool for _, cookie := range cookies {