mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-08 06:54:24 +00:00
Net: strip sensitive headers on cross-origin redirects
This commit is contained in:
committed by
George Pickett
parent
eec5a6d6f1
commit
c0cd5a7265
@@ -8,6 +8,10 @@ function redirectResponse(location: string): Response {
|
||||
});
|
||||
}
|
||||
|
||||
function okResponse(body = "ok"): Response {
|
||||
return new Response(body, { status: 200 });
|
||||
}
|
||||
|
||||
describe("fetchWithSsrFGuard hardening", () => {
|
||||
type LookupFn = NonNullable<Parameters<typeof fetchWithSsrFGuard>[0]["lookupFn"]>;
|
||||
|
||||
@@ -88,4 +92,60 @@ describe("fetchWithSsrFGuard hardening", () => {
|
||||
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
||||
await result.release();
|
||||
});
|
||||
|
||||
it("strips sensitive headers when redirect crosses origins", async () => {
|
||||
const lookupFn = vi.fn(async () => [
|
||||
{ address: "93.184.216.34", family: 4 },
|
||||
]) as unknown as LookupFn;
|
||||
const fetchImpl = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(redirectResponse("https://cdn.example.com/asset"))
|
||||
.mockResolvedValueOnce(okResponse());
|
||||
|
||||
const result = await fetchWithSsrFGuard({
|
||||
url: "https://api.example.com/start",
|
||||
fetchImpl,
|
||||
lookupFn,
|
||||
init: {
|
||||
headers: {
|
||||
Authorization: "Bearer secret",
|
||||
Cookie: "session=abc",
|
||||
"X-Trace": "1",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const [, secondInit] = fetchImpl.mock.calls[1] as [string, RequestInit];
|
||||
const headers = new Headers(secondInit.headers);
|
||||
expect(headers.get("authorization")).toBeNull();
|
||||
expect(headers.get("cookie")).toBeNull();
|
||||
expect(headers.get("x-trace")).toBe("1");
|
||||
await result.release();
|
||||
});
|
||||
|
||||
it("keeps headers when redirect stays on same origin", async () => {
|
||||
const lookupFn = vi.fn(async () => [
|
||||
{ address: "93.184.216.34", family: 4 },
|
||||
]) as unknown as LookupFn;
|
||||
const fetchImpl = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(redirectResponse("/next"))
|
||||
.mockResolvedValueOnce(okResponse());
|
||||
|
||||
const result = await fetchWithSsrFGuard({
|
||||
url: "https://api.example.com/start",
|
||||
fetchImpl,
|
||||
lookupFn,
|
||||
init: {
|
||||
headers: {
|
||||
Authorization: "Bearer secret",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const [, secondInit] = fetchImpl.mock.calls[1] as [string, RequestInit];
|
||||
const headers = new Headers(secondInit.headers);
|
||||
expect(headers.get("authorization")).toBe("Bearer secret");
|
||||
await result.release();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,11 +32,28 @@ export type GuardedFetchResult = {
|
||||
};
|
||||
|
||||
const DEFAULT_MAX_REDIRECTS = 3;
|
||||
const CROSS_ORIGIN_REDIRECT_SENSITIVE_HEADERS = [
|
||||
"authorization",
|
||||
"proxy-authorization",
|
||||
"cookie",
|
||||
"cookie2",
|
||||
];
|
||||
|
||||
function isRedirectStatus(status: number): boolean {
|
||||
return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
|
||||
}
|
||||
|
||||
function stripSensitiveHeadersForCrossOriginRedirect(init?: RequestInit): RequestInit | undefined {
|
||||
if (!init?.headers) {
|
||||
return init;
|
||||
}
|
||||
const headers = new Headers(init.headers);
|
||||
for (const header of CROSS_ORIGIN_REDIRECT_SENSITIVE_HEADERS) {
|
||||
headers.delete(header);
|
||||
}
|
||||
return { ...init, headers };
|
||||
}
|
||||
|
||||
function buildAbortSignal(params: { timeoutMs?: number; signal?: AbortSignal }): {
|
||||
signal?: AbortSignal;
|
||||
cleanup: () => void;
|
||||
@@ -99,6 +116,7 @@ export async function fetchWithSsrFGuard(params: GuardedFetchOptions): Promise<G
|
||||
|
||||
const visited = new Set<string>();
|
||||
let currentUrl = params.url;
|
||||
let currentInit = params.init ? { ...params.init } : undefined;
|
||||
let redirectCount = 0;
|
||||
|
||||
while (true) {
|
||||
@@ -125,7 +143,7 @@ export async function fetchWithSsrFGuard(params: GuardedFetchOptions): Promise<G
|
||||
}
|
||||
|
||||
const init: RequestInit & { dispatcher?: Dispatcher } = {
|
||||
...(params.init ? { ...params.init } : {}),
|
||||
...(currentInit ? { ...currentInit } : {}),
|
||||
redirect: "manual",
|
||||
...(dispatcher ? { dispatcher } : {}),
|
||||
...(signal ? { signal } : {}),
|
||||
@@ -144,11 +162,15 @@ export async function fetchWithSsrFGuard(params: GuardedFetchOptions): Promise<G
|
||||
await release(dispatcher);
|
||||
throw new Error(`Too many redirects (limit: ${maxRedirects})`);
|
||||
}
|
||||
const nextUrl = new URL(location, parsedUrl).toString();
|
||||
const nextParsedUrl = new URL(location, parsedUrl);
|
||||
const nextUrl = nextParsedUrl.toString();
|
||||
if (visited.has(nextUrl)) {
|
||||
await release(dispatcher);
|
||||
throw new Error("Redirect loop detected");
|
||||
}
|
||||
if (nextParsedUrl.origin !== parsedUrl.origin) {
|
||||
currentInit = stripSensitiveHeadersForCrossOriginRedirect(currentInit);
|
||||
}
|
||||
visited.add(nextUrl);
|
||||
void response.body?.cancel();
|
||||
await closeDispatcher(dispatcher);
|
||||
|
||||
Reference in New Issue
Block a user