Skip to content

Commit c380447

Browse files
rexxarsbartlomieju
andcommitted
Merge commit from fork
* fix: drop auth headers, cookies on redirect to different origin * refactor: destructure StringPrototypeEndsWith --------- Co-authored-by: Bartek Iwańczuk <[email protected]>
1 parent 60760ab commit c380447

File tree

2 files changed

+97
-1
lines changed

2 files changed

+97
-1
lines changed

ext/fetch/26_fetch.js

+45-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const {
3131
SafeArrayIterator,
3232
SafePromisePrototypeFinally,
3333
String,
34+
StringPrototypeEndsWith,
3435
StringPrototypeSlice,
3536
StringPrototypeStartsWith,
3637
StringPrototypeToLowerCase,
@@ -66,6 +67,12 @@ const REQUEST_BODY_HEADER_NAMES = [
6667
"content-type",
6768
];
6869

70+
const REDIRECT_SENSITIVE_HEADER_NAMES = [
71+
"authorization",
72+
"proxy-authorization",
73+
"cookie",
74+
];
75+
6976
/**
7077
* @param {number} rid
7178
* @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) {
253260
if (locationHeaders.length === 0) {
254261
return response;
255262
}
263+
264+
const currentURL = new URL(request.currentUrl());
256265
const locationURL = new URL(
257266
locationHeaders[0][1],
258267
response.url() ?? undefined,
259268
);
260269
if (locationURL.hash === "") {
261-
locationURL.hash = request.currentUrl().hash;
270+
locationURL.hash = currentURL.hash;
262271
}
263272
if (locationURL.protocol !== "https:" && locationURL.protocol !== "http:") {
264273
return networkError("Can not redirect to a non HTTP(s) url");
@@ -297,6 +306,28 @@ function httpRedirectFetch(request, response, terminator) {
297306
}
298307
}
299308
}
309+
310+
// Drop confidential headers when redirecting to a less secure protocol
311+
// or to a different domain that is not a superdomain
312+
if (
313+
locationURL.protocol !== currentURL.protocol &&
314+
locationURL.protocol !== "https:" ||
315+
locationURL.host !== currentURL.host &&
316+
!isSubdomain(locationURL.host, currentURL.host)
317+
) {
318+
for (let i = 0; i < request.headerList.length; i++) {
319+
if (
320+
ArrayPrototypeIncludes(
321+
REDIRECT_SENSITIVE_HEADER_NAMES,
322+
byteLowerCase(request.headerList[i][0]),
323+
)
324+
) {
325+
ArrayPrototypeSplice(request.headerList, i, 1);
326+
i--;
327+
}
328+
}
329+
}
330+
300331
if (request.body !== null) {
301332
const res = extractBody(request.body.source);
302333
request.body = res.body;
@@ -470,6 +501,19 @@ function abortFetch(request, responseObject, error) {
470501
return error;
471502
}
472503

504+
/**
505+
* Checks if the given string is a subdomain of the given domain.
506+
*
507+
* @param {String} subdomain
508+
* @param {String} domain
509+
* @returns {Boolean}
510+
*/
511+
function isSubdomain(subdomain, domain) {
512+
const dot = subdomain.length - domain.length - 1;
513+
return dot > 0 && subdomain[dot] === "." &&
514+
StringPrototypeEndsWith(subdomain, domain);
515+
}
516+
473517
/**
474518
* Handle the Response argument to the WebAssembly streaming APIs, after
475519
* resolving if it was passed as a promise. This function should be registered

tests/unit/fetch_test.ts

+52
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,58 @@ Deno.test(
439439
},
440440
);
441441

442+
Deno.test(
443+
{
444+
permissions: { net: true },
445+
},
446+
async function fetchWithAuthorizationHeaderRedirection() {
447+
const response = await fetch("http://localhost:4546/echo_server", {
448+
headers: { authorization: "Bearer foo" },
449+
});
450+
assertEquals(response.status, 200);
451+
assertEquals(response.statusText, "OK");
452+
assertEquals(response.url, "http://localhost:4545/echo_server");
453+
assertEquals(response.headers.get("authorization"), null);
454+
assertEquals(await response.text(), "");
455+
},
456+
);
457+
458+
Deno.test(
459+
{
460+
permissions: { net: true },
461+
},
462+
async function fetchWithCookieHeaderRedirection() {
463+
const response = await fetch("http://localhost:4546/echo_server", {
464+
headers: { Cookie: "sessionToken=verySecret" },
465+
});
466+
assertEquals(response.status, 200);
467+
assertEquals(response.statusText, "OK");
468+
assertEquals(response.url, "http://localhost:4545/echo_server");
469+
assertEquals(response.headers.get("cookie"), null);
470+
assertEquals(await response.text(), "");
471+
},
472+
);
473+
474+
Deno.test(
475+
{
476+
permissions: { net: true },
477+
},
478+
async function fetchWithProxyAuthorizationHeaderRedirection() {
479+
const response = await fetch("http://localhost:4546/echo_server", {
480+
headers: {
481+
"proxy-authorization": "Basic ZXNwZW46a29rb3M=",
482+
"accept": "application/json",
483+
},
484+
});
485+
assertEquals(response.status, 200);
486+
assertEquals(response.statusText, "OK");
487+
assertEquals(response.url, "http://localhost:4545/echo_server");
488+
assertEquals(response.headers.get("proxy-authorization"), null);
489+
assertEquals(response.headers.get("accept"), "application/json");
490+
assertEquals(await response.text(), "");
491+
},
492+
);
493+
442494
Deno.test(
443495
{ permissions: { net: true } },
444496
async function fetchInitStringBody() {

0 commit comments

Comments
 (0)