node-fetch / node-fetch Public
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
fix(Headers): don't forward secure headers to 3th party #1449
fix(Headers): don't forward secure headers to 3th party #1449
Conversation
src/utils/is.js
Outdated
if (!a.endsWith(b)) { | ||
return false; | ||
} | ||
|
||
return a[a.length - b.length - 1] === '.'; |
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.
What do you think about adding the .
to the endsWith
call?
if (!a.endsWith(b)) { | |
return false; | |
} | |
return a[a.length - b.length - 1] === '.'; | |
return a.endsWith(`.${b}`) |
I think that this is much easier to read
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.
if you are making a request to fridas-blog.uk.com
and it redirects to uk.com
then uk.com
should not know about the cookie... cuz the cookie can be tied to fridas-blog.uk.com
a
= referer (original request) fridas-blog.uk.com
b
= destination (Location) uk.com
a.endsWith('.${b}')
=== true
b
is not the same host or a subdomain of a
...
so i guess it can't work...
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.
if you are only after something that is sorter and dose the same thing:
export const isDomainOrSubdomain = (a, b) => {
a = new URL(a).hostname
b = new URL(b).hostname
return a === b || (
a[a.length - b.length - 1] === '.' && a.endsWith(b)
)
}
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 still don't understand how the two checks are different? As far as I can tell, unless I'm missing something, the two functions below behave the exact same way?
function one (a, b) {
return a === b || (
a[a.length - b.length - 1] === '.' && a.endsWith(b)
)
}
function two (a, b) {
return a === b || a.endsWith(`.${b}`)
}
The case you are mentioning seems to be handled wrong in the committed code then?
> isDomainOrSubdomain('http://uk.com', 'http://fridas-blog.uk.com')
true
(extra ping @jimmywarting since this is already merged)
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.
hmm, double checked again and you are right... they are equally the same, But there is no mistake in the merged code...
I was blinded by the order of arguments passed down to isDomainOrSubdomain
and mixing up whats gets passed down to the function and in which order. i was just so blinded by how Go and follow-redirects also did it using the dot index
But at least there is no rush to change it now... can change it later
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.
a & b was a stupid variable name...
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.
Nice
Submitted PR to make the code easier to understand here: #1455
Looks good to me. Feel free to refactor the isDomainOrSubdomain
method to make it easier to read, but it's good as is from my perspective.
Will you do that yourself or shall we do a follow up issue so we don't forget? |
Thx for everyone to get to this, this issue can finally be closed :) But do note the drawback: |
I can fix V2 also |
@@ -56,3 +56,20 @@ export const isAbortSignal = object => { | |||
) | |||
); | |||
}; | |||
|
|||
/** | |||
* isDomainOrSubdomain reports whether sub is a subdomain (or exact match) of |
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.
Should this use the public suffix list instead? https://publicsuffix.org/
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 don't think that would be necessary.
* @param {string|URL} original | ||
* @param {string|URL} destination | ||
*/ | ||
export const isDomainOrSubdomain = (destination, original) => { |
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.
The names of the variables here don't see to match the values passed in?
if (!isDomainOrSubdomain(request.url, locationURL)) {
Surely locationURL is the destination and request.url is the original?
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 think you are right. it works as intended and how it is suppose to protect you, But the variable names are mixed up
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.
No rush to get fixed, can create a issue about it if needed
This change may match Go behavior but it differs from browser behavior and deviates from the fetch API spec. In Chrome, if I do:
and this results in a redirect to a third party, Chrome will forward the Authorization header to the third party, assuming that the third party's CORS policy allows it. Note that the CORS policy of the original URL is irrelevant -- the third party controls the policy here. So, in an attack scenario, the attacker controls the policy, so CORS is not a defense. AFAICT, Chrome's behavior conforms to the Fetch spec. The Fetch spec only says these headers should be removed on a redirect if they were implicitly added by the user-agent in the first place -- i.e. not when they were explicitly passed to I am concerned that this change could actually break legitimate use cases. Imagine |
That may be legit cases... I have also wonder about the usage of Request.credentials if we somehow could use this in any way. The option Xhr have withCredentials |
|
@kentonv (Hi!) Just to check, your concern is specifically about Authorization, right? It does seem reasonable to me that cookies should not be sent to a different origin. |
Hi @glasser! Long time no see. TBH, I'm not really sure what this should mean for What makes sense on the server side? I don't really know because I don't know under what circumstances a server would ever send a For Cloudflare Workers (of which I'm the lead engineer), our A Worker can also make a stand-alone FWIW, at present Cloudflare Workers has no special logic whatsoever for Of course, for the API use case, it's very common for other headers to contain sensitive information, too. Some APIs use non-standard headers like So in the absence of more information about real-world use cases, my inclination is to say that there's no reason to treat |
|
Interestingly, here's a Stack Overflow question from a real user who legitimately tried to redirect an API, and was frustrated that the https://stackoverflow.com/questions/71441897/cloudflare-worker-redirect-stripping-auth-headers/ |
yea, indeed interesting... a real problem. i really do think it should be up to the developer to say to the http client that it should explicitly forward insecure headers in case of a redirect. I'm not sure if I would trust the api to keep my credentials safe and sound if some malicious user finds a way to redirect some endpoint to a 3th party domain and steal my credentials. i think every http client, http, https, http2, curl, wget go, ruby and everything else should have something some kind of option to choose from when making the request in case of a redirect to a other 3th party domain fetch(url, {
forward_headers_and_body_when_redirect_to_untrusted_domain: true // false by default
}) if it don't work in the browser or Go HTTP anyway then you are trying to solve the problem in a wrong way anyway that don't work for all http client, there could be other api users that use some other http client that also don't support forwarding secure headers there is also other ways to tackle the problem like I almost do think authentication header should be forwared but not cookies cuz they are set to a specific domain/path |
maybe Alt-Svc could be a solution? |
No no, to reiterate: Browsers do forward the However, given that so many other HTTP client libraries have implemented this behavior, I guess realistically it just isn't possible to redirect an API across domains today, so maybe that use case is moot. |
Do you know what alt-sve will do with cookies and other headers if it decided to try another url the next time? |
Purpose
Avoid forwarding secure headers such as authorization, www-authenticate, cookie & cookie2 when redirecting to a untrusted site. (so it follows browses origin policy)
(matches Go behavior)
Changes
When the location headers redirects to an other hostname, strip away secure request headers meant for the original destination
Additional
Should patch v2 also (not many can update to esm)