-
Notifications
You must be signed in to change notification settings - Fork 943
fix(site): prevent ExtractAPIKey from dirtying the HTML output #8450
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 4 commits
630a40a
54406d9
3180dbc
68f3aa9
a3c408e
b70084d
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 |
---|---|---|
|
@@ -265,7 +265,7 @@ func ShouldCacheFile(reqFile string) bool { | |
} | ||
|
||
func (h *Handler) serveHTML(resp http.ResponseWriter, request *http.Request, reqPath string, state htmlState) bool { | ||
if data, err := h.renderHTMLWithState(resp, request, reqPath, state); err == nil { | ||
if data, err := h.renderHTMLWithState(request, reqPath, state); err == nil { | ||
if reqPath == "" { | ||
// Pass "index.html" to the ServeContent so the ServeContent sets the right content headers. | ||
reqPath = "index.html" | ||
|
@@ -278,7 +278,7 @@ func (h *Handler) serveHTML(resp http.ResponseWriter, request *http.Request, req | |
|
||
// renderWithState will render the file using the given nonce if the file exists | ||
// as a template. If it does not, it will return an error. | ||
func (h *Handler) renderHTMLWithState(rw http.ResponseWriter, r *http.Request, filePath string, state htmlState) ([]byte, error) { | ||
func (h *Handler) renderHTMLWithState(r *http.Request, filePath string, state htmlState) ([]byte, error) { | ||
var buf bytes.Buffer | ||
if filePath == "" { | ||
filePath = "index.html" | ||
|
@@ -290,7 +290,11 @@ func (h *Handler) renderHTMLWithState(rw http.ResponseWriter, r *http.Request, f | |
|
||
// Cookies are sent when requesting HTML, so we can get the user | ||
// and pre-populate the state for the frontend to reduce requests. | ||
apiKey, actor, _ := httpmw.ExtractAPIKey(rw, r, httpmw.ExtractAPIKeyConfig{ | ||
// We use a noop response writer because we don't want to write | ||
// anything to the response and break the HTML, an error means we | ||
// simply don't pre-populate the state. | ||
noopRW := noopResponseWriter{} | ||
apiKey, actor, ok := httpmw.ExtractAPIKey(noopRW, r, httpmw.ExtractAPIKeyConfig{ | ||
Comment on lines
+296
to
+297
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. This makes a lot of sense. I think the FE has code where if a static value is not populated statically, it defaults to using the API. so there should never be an error, just an empty static field. |
||
Optional: true, | ||
DB: h.opts.Database, | ||
OAuth2Configs: h.opts.OAuth2Configs, | ||
|
@@ -300,7 +304,7 @@ func (h *Handler) renderHTMLWithState(rw http.ResponseWriter, r *http.Request, f | |
RedirectToLogin: false, | ||
SessionTokenFunc: nil, | ||
}) | ||
if apiKey != nil && actor != nil { | ||
if ok && apiKey != nil && actor != nil { | ||
ctx := dbauthz.As(r.Context(), actor.Actor) | ||
|
||
var eg errgroup.Group | ||
|
@@ -392,6 +396,13 @@ func (h *Handler) renderHTMLWithState(rw http.ResponseWriter, r *http.Request, f | |
return buf.Bytes(), nil | ||
} | ||
|
||
// noopResponseWriter is a response writer that does nothing. | ||
type noopResponseWriter struct{} | ||
|
||
func (noopResponseWriter) Header() http.Header { return http.Header{} } | ||
func (noopResponseWriter) Write(p []byte) (int, error) { return len(p), nil } | ||
func (noopResponseWriter) WriteHeader(int) {} | ||
|
||
// secureHeaders is only needed for statically served files. We do not need this for api endpoints. | ||
// It adds various headers to enforce browser security features. | ||
func secureHeaders() *secure.Secure { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -69,6 +69,53 @@ func TestInjection(t *testing.T) { | |
require.Equal(t, db2sdk.User(user, []uuid.UUID{}), got) | ||
} | ||
|
||
func TestInjectionFailureProducesCleanHTML(t *testing.T) { | ||
t.Parallel() | ||
|
||
db := dbfake.New() | ||
|
||
// Create an expired user with a refresh token, but provide no OAuth2 | ||
// configuration so that refresh is impossible, this should result in | ||
// an error when httpmw.ExtractAPIKey is called. | ||
user := dbgen.User(t, db, database.User{}) | ||
_, token := dbgen.APIKey(t, db, database.APIKey{ | ||
UserID: user.ID, | ||
LastUsed: database.Now().Add(-time.Hour), | ||
ExpiresAt: database.Now().Add(-time.Second), | ||
LoginType: database.LoginTypeGithub, | ||
}) | ||
_ = dbgen.UserLink(t, db, database.UserLink{ | ||
UserID: user.ID, | ||
LoginType: database.LoginTypeGithub, | ||
OAuthRefreshToken: "hello", | ||
OAuthExpiry: database.Now().Add(-time.Second), | ||
}) | ||
|
||
binFs := http.FS(fstest.MapFS{}) | ||
siteFS := fstest.MapFS{ | ||
"index.html": &fstest.MapFile{ | ||
Data: []byte("<html>{{ .User }}</html>"), | ||
}, | ||
} | ||
Comment on lines
+96
to
+100
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. TIL about |
||
handler := site.New(&site.Options{ | ||
BinFS: binFs, | ||
Database: db, | ||
SiteFS: siteFS, | ||
}) | ||
|
||
r := httptest.NewRequest("GET", "/", nil) | ||
r.Header.Set(codersdk.SessionTokenHeader, token) | ||
rw := httptest.NewRecorder() | ||
|
||
handler.ServeHTTP(rw, r) | ||
|
||
// Ensure we get a clean HTML response with no user data or errors | ||
// from httpmw.ExtractAPIKey. | ||
require.Equal(t, http.StatusOK, rw.Code) | ||
body := rw.Body.String() | ||
require.Equal(t, "<html></html>", body) | ||
} | ||
|
||
func TestCaching(t *testing.T) { | ||
t.Parallel() | ||
|
||
|
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 it possible to add a unit test that ossifies this behaviour? It would be sad if we had another regression on this.
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.
I agree, I'm looking into this currently 👍🏻
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.
Covered in
54406d9
(#8450):