1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-12 02:27:46 -05:00

Merge commit from fork

* fix: drop auth headers, cookies on redirect to different origin

* refactor: destructure StringPrototypeEndsWith

---------

Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
This commit is contained in:
Espen Hovlandsdal 2024-11-25 10:07:00 -08:00 committed by Bartek Iwańczuk
parent 60760abd87
commit c380447de7
No known key found for this signature in database
GPG key ID: 0C6BCDDC3B3AD750
2 changed files with 97 additions and 1 deletions

View file

@ -31,6 +31,7 @@ const {
SafeArrayIterator, SafeArrayIterator,
SafePromisePrototypeFinally, SafePromisePrototypeFinally,
String, String,
StringPrototypeEndsWith,
StringPrototypeSlice, StringPrototypeSlice,
StringPrototypeStartsWith, StringPrototypeStartsWith,
StringPrototypeToLowerCase, StringPrototypeToLowerCase,
@ -66,6 +67,12 @@ const REQUEST_BODY_HEADER_NAMES = [
"content-type", "content-type",
]; ];
const REDIRECT_SENSITIVE_HEADER_NAMES = [
"authorization",
"proxy-authorization",
"cookie",
];
/** /**
* @param {number} rid * @param {number} rid
* @returns {Promise<{ status: number, statusText: string, headers: [string, string][], url: string, responseRid: number, error: [string, string]? }>} * @returns {Promise<{ status: number, statusText: string, headers: [string, string][], url: string, responseRid: number, error: [string, string]? }>}
@ -253,12 +260,14 @@ function httpRedirectFetch(request, response, terminator) {
if (locationHeaders.length === 0) { if (locationHeaders.length === 0) {
return response; return response;
} }
const currentURL = new URL(request.currentUrl());
const locationURL = new URL( const locationURL = new URL(
locationHeaders[0][1], locationHeaders[0][1],
response.url() ?? undefined, response.url() ?? undefined,
); );
if (locationURL.hash === "") { if (locationURL.hash === "") {
locationURL.hash = request.currentUrl().hash; locationURL.hash = currentURL.hash;
} }
if (locationURL.protocol !== "https:" && locationURL.protocol !== "http:") { if (locationURL.protocol !== "https:" && locationURL.protocol !== "http:") {
return networkError("Can not redirect to a non HTTP(s) url"); return networkError("Can not redirect to a non HTTP(s) url");
@ -297,6 +306,28 @@ function httpRedirectFetch(request, response, terminator) {
} }
} }
} }
// Drop confidential headers when redirecting to a less secure protocol
// or to a different domain that is not a superdomain
if (
locationURL.protocol !== currentURL.protocol &&
locationURL.protocol !== "https:" ||
locationURL.host !== currentURL.host &&
!isSubdomain(locationURL.host, currentURL.host)
) {
for (let i = 0; i < request.headerList.length; i++) {
if (
ArrayPrototypeIncludes(
REDIRECT_SENSITIVE_HEADER_NAMES,
byteLowerCase(request.headerList[i][0]),
)
) {
ArrayPrototypeSplice(request.headerList, i, 1);
i--;
}
}
}
if (request.body !== null) { if (request.body !== null) {
const res = extractBody(request.body.source); const res = extractBody(request.body.source);
request.body = res.body; request.body = res.body;
@ -470,6 +501,19 @@ function abortFetch(request, responseObject, error) {
return error; return error;
} }
/**
* Checks if the given string is a subdomain of the given domain.
*
* @param {String} subdomain
* @param {String} domain
* @returns {Boolean}
*/
function isSubdomain(subdomain, domain) {
const dot = subdomain.length - domain.length - 1;
return dot > 0 && subdomain[dot] === "." &&
StringPrototypeEndsWith(subdomain, domain);
}
/** /**
* Handle the Response argument to the WebAssembly streaming APIs, after * Handle the Response argument to the WebAssembly streaming APIs, after
* resolving if it was passed as a promise. This function should be registered * resolving if it was passed as a promise. This function should be registered

View file

@ -439,6 +439,58 @@ Deno.test(
}, },
); );
Deno.test(
{
permissions: { net: true },
},
async function fetchWithAuthorizationHeaderRedirection() {
const response = await fetch("http://localhost:4546/echo_server", {
headers: { authorization: "Bearer foo" },
});
assertEquals(response.status, 200);
assertEquals(response.statusText, "OK");
assertEquals(response.url, "http://localhost:4545/echo_server");
assertEquals(response.headers.get("authorization"), null);
assertEquals(await response.text(), "");
},
);
Deno.test(
{
permissions: { net: true },
},
async function fetchWithCookieHeaderRedirection() {
const response = await fetch("http://localhost:4546/echo_server", {
headers: { Cookie: "sessionToken=verySecret" },
});
assertEquals(response.status, 200);
assertEquals(response.statusText, "OK");
assertEquals(response.url, "http://localhost:4545/echo_server");
assertEquals(response.headers.get("cookie"), null);
assertEquals(await response.text(), "");
},
);
Deno.test(
{
permissions: { net: true },
},
async function fetchWithProxyAuthorizationHeaderRedirection() {
const response = await fetch("http://localhost:4546/echo_server", {
headers: {
"proxy-authorization": "Basic ZXNwZW46a29rb3M=",
"accept": "application/json",
},
});
assertEquals(response.status, 200);
assertEquals(response.statusText, "OK");
assertEquals(response.url, "http://localhost:4545/echo_server");
assertEquals(response.headers.get("proxy-authorization"), null);
assertEquals(response.headers.get("accept"), "application/json");
assertEquals(await response.text(), "");
},
);
Deno.test( Deno.test(
{ permissions: { net: true } }, { permissions: { net: true } },
async function fetchInitStringBody() { async function fetchInitStringBody() {