[arvados] created: 2.7.0-5082-g9b9ada2248
git repository hosting
git at public.arvados.org
Thu Oct 26 20:16:58 UTC 2023
at 9b9ada224856e289cdd9e81954c4ea3c3bc1fe68 (commit)
commit 9b9ada224856e289cdd9e81954c4ea3c3bc1fe68
Author: Brett Smith <brett.smith at curii.com>
Date: Thu Oct 26 16:13:31 2023 -0400
21025: Handle api_token as a list throughout keep-web handler
The previous code used `Request.FormValue("api_token") == ""` to
determine whether or not an API token was provided. However, this empty
string value could be returned both when there was no API token, or if
an attacker explicitly passed `api_token=`. An attacker could take
advantage of this flattening to bypass the intended redirect and
introspect API tokens in the URL.
Arvados-DCO-1.1-Signed-off-by: Brett Smith <brett.smith at curii.com>
diff --git a/services/keep-web/handler.go b/services/keep-web/handler.go
index 3af326a1ad..0df19d443d 100644
--- a/services/keep-web/handler.go
+++ b/services/keep-web/handler.go
@@ -293,12 +293,16 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
reqTokens = auth.CredentialsFromRequest(r).Tokens
}
- formToken := r.FormValue("api_token")
+ r.ParseForm()
origin := r.Header.Get("Origin")
cors := origin != "" && !strings.HasSuffix(origin, "://"+r.Host)
safeAjax := cors && (r.Method == http.MethodGet || r.Method == http.MethodHead)
- safeAttachment := attachment && r.URL.Query().Get("api_token") == ""
- if formToken == "" {
+ // Important distiction: safeAttachment checks whether api_token exists as
+ // a query parameter. The following condition checks whether api_token
+ // exists as request form data *or* a query parameter. This distinction is
+ // necessary to redirect when required, and not when not.
+ safeAttachment := attachment && !r.URL.Query().Has("api_token")
+ if formTokens, haveFormTokens := r.Form["api_token"]; !haveFormTokens {
// No token to use or redact.
} else if safeAjax || safeAttachment {
// If this is a cross-origin request, the URL won't
@@ -313,7 +317,9 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
// form?" problem, so provided the token isn't
// embedded in the URL, there's no reason to do
// redirect-with-cookie in this case either.
- reqTokens = append(reqTokens, formToken)
+ for _, tok := range formTokens {
+ reqTokens = append(reqTokens, tok)
+ }
} else if browserMethod[r.Method] {
// If this is a page view, and the client provided a
// token via query string or POST body, we must put
@@ -776,7 +782,7 @@ func applyContentDispositionHdr(w http.ResponseWriter, r *http.Request, filename
}
func (h *handler) seeOtherWithCookie(w http.ResponseWriter, r *http.Request, location string, credentialsOK bool) {
- if formToken := r.FormValue("api_token"); formToken != "" {
+ if formTokens, haveFormTokens := r.Form["api_token"]; haveFormTokens {
if !credentialsOK {
// It is not safe to copy the provided token
// into a cookie unless the current vhost
@@ -797,13 +803,19 @@ func (h *handler) seeOtherWithCookie(w http.ResponseWriter, r *http.Request, loc
// bar, and in the case of a POST request to avoid
// raising warnings when the user refreshes the
// resulting page.
- http.SetCookie(w, &http.Cookie{
- Name: "arvados_api_token",
- Value: auth.EncodeTokenCookie([]byte(formToken)),
- Path: "/",
- HttpOnly: true,
- SameSite: http.SameSiteLaxMode,
- })
+ for _, tok := range formTokens {
+ if tok == "" {
+ continue
+ }
+ http.SetCookie(w, &http.Cookie{
+ Name: "arvados_api_token",
+ Value: auth.EncodeTokenCookie([]byte(tok)),
+ Path: "/",
+ HttpOnly: true,
+ SameSite: http.SameSiteLaxMode,
+ })
+ break
+ }
}
// Propagate query parameters (except api_token) from
diff --git a/services/keep-web/handler_test.go b/services/keep-web/handler_test.go
index 4a76276392..5e81f3a01e 100644
--- a/services/keep-web/handler_test.go
+++ b/services/keep-web/handler_test.go
@@ -797,6 +797,34 @@ func (s *IntegrationSuite) TestVhostRedirectQueryTokenAttachmentOnlyHost(c *chec
c.Check(resp.Header().Get("Content-Disposition"), check.Equals, "attachment")
}
+func (s *IntegrationSuite) TestVhostRedirectMultipleTokens(c *check.C) {
+ baseUrl := arvadostest.FooCollection + ".example.com/foo"
+ query := url.Values{}
+
+ // The intent of these tests is to check that requests are redirected
+ // correctly in the presence of multiple API tokens. The exact response
+ // codes and content are not closely considered: they're just how
+ // keep-web responded when we made the smallest possible fix. Changing
+ // those responses may be okay, but you should still test all these
+ // different cases and the associated redirect logic.
+ query["api_token"] = []string{arvadostest.ActiveToken, arvadostest.AnonymousToken}
+ s.testVhostRedirectTokenToCookie(c, "GET", baseUrl, "?"+query.Encode(), nil, "", http.StatusOK, "foo")
+ query["api_token"] = []string{arvadostest.ActiveToken, arvadostest.AnonymousToken, ""}
+ s.testVhostRedirectTokenToCookie(c, "GET", baseUrl, "?"+query.Encode(), nil, "", http.StatusOK, "foo")
+ query["api_token"] = []string{arvadostest.ActiveToken, "", arvadostest.AnonymousToken}
+ s.testVhostRedirectTokenToCookie(c, "GET", baseUrl, "?"+query.Encode(), nil, "", http.StatusOK, "foo")
+ query["api_token"] = []string{"", arvadostest.ActiveToken}
+ s.testVhostRedirectTokenToCookie(c, "GET", baseUrl, "?"+query.Encode(), nil, "", http.StatusOK, "foo")
+
+ expectContent := regexp.QuoteMeta(unauthorizedMessage + "\n")
+ query["api_token"] = []string{arvadostest.AnonymousToken, "invalidtoo"}
+ s.testVhostRedirectTokenToCookie(c, "GET", baseUrl, "?"+query.Encode(), nil, "", http.StatusUnauthorized, expectContent)
+ query["api_token"] = []string{arvadostest.AnonymousToken, ""}
+ s.testVhostRedirectTokenToCookie(c, "GET", baseUrl, "?"+query.Encode(), nil, "", http.StatusUnauthorized, expectContent)
+ query["api_token"] = []string{"", arvadostest.AnonymousToken}
+ s.testVhostRedirectTokenToCookie(c, "GET", baseUrl, "?"+query.Encode(), nil, "", http.StatusUnauthorized, expectContent)
+}
+
func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie(c *check.C) {
s.testVhostRedirectTokenToCookie(c, "POST",
arvadostest.FooCollection+".example.com/foo",
@@ -999,20 +1027,36 @@ func (s *IntegrationSuite) testVhostRedirectTokenToCookie(c *check.C, method, ho
s.handler.ServeHTTP(resp, req)
if resp.Code != http.StatusSeeOther {
+ attachment, _ := regexp.MatchString(`^attachment(;|$)`, resp.Header().Get("Content-Disposition"))
+ // Since we're not redirecting, check that any api_token in the URL is
+ // handled safely.
+ // If there is no token in the URL, then we're good.
+ // Otherwise, if the response code is an error, the body is expected to
+ // be static content, and nothing that might maliciously introspect the
+ // URL. It's considered safe and allowed.
+ // Otherwise, if the response content has attachment disposition,
+ // that's considered safe for all the reasons explained in the
+ // safeAttachment comment in handler.go.
+ c.Check(!u.Query().Has("api_token") || resp.Code >= 400 || attachment, check.Equals, true)
return resp
}
+
+ loc, err := url.Parse(resp.Header().Get("Location"))
+ c.Assert(err, check.IsNil)
+ c.Check(loc.Scheme, check.Equals, u.Scheme)
+ c.Check(loc.Host, check.Equals, u.Host)
+ c.Check(loc.RawPath, check.Equals, u.RawPath)
+ // If the response was a redirect, it should never include an API token.
+ c.Check(loc.Query().Has("api_token"), check.Equals, false)
c.Check(resp.Body.String(), check.Matches, `.*href="http://`+regexp.QuoteMeta(html.EscapeString(hostPath))+`(\?[^"]*)?".*`)
- c.Check(strings.Split(resp.Header().Get("Location"), "?")[0], check.Equals, "http://"+hostPath)
cookies := (&http.Response{Header: resp.Header()}).Cookies()
- u, err := u.Parse(resp.Header().Get("Location"))
- c.Assert(err, check.IsNil)
c.Logf("following redirect to %s", u)
req = &http.Request{
Method: "GET",
- Host: u.Host,
- URL: u,
- RequestURI: u.RequestURI(),
+ Host: loc.Host,
+ URL: loc,
+ RequestURI: loc.RequestURI(),
Header: reqHeader,
}
for _, c := range cookies {
-----------------------------------------------------------------------
hooks/post-receive
--
More information about the arvados-commits
mailing list