diff --git a/.changelog/strip-cross-origin-credentials.md b/.changelog/strip-cross-origin-credentials.md new file mode 100644 index 0000000..a1f436c --- /dev/null +++ b/.changelog/strip-cross-origin-credentials.md @@ -0,0 +1,5 @@ +--- +wallet-cli: patch +--- + +Strip credential-bearing headers when `tempo request` follows a cross-origin redirect. diff --git a/src/commands/request.ts b/src/commands/request.ts index 0a5c0ad..7961546 100644 --- a/src/commands/request.ts +++ b/src/commands/request.ts @@ -474,11 +474,18 @@ async function fetchWithRedirects( } } -function redirectRequest(request: FetchPlan, status: number, location: string): FetchPlan { - const nextUrl = new URL(location, request.url).toString(); +export function redirectRequest(request: FetchPlan, status: number, location: string): FetchPlan { + const nextUrl = new URL(location, request.url); const init = cloneRequestInit(request.init); + const headers = new Headers(init.headers); const method = init.method?.toUpperCase() ?? "GET"; + if (new URL(request.url).origin !== nextUrl.origin) { + headers.delete("authorization"); + headers.delete("cookie"); + headers.delete("proxy-authorization"); + } + if ( (status === 301 || status === 302 || status === 303) && method !== "GET" && @@ -486,13 +493,12 @@ function redirectRequest(request: FetchPlan, status: number, location: string): ) { init.method = "GET"; delete init.body; - const headers = new Headers(init.headers); headers.delete("content-length"); headers.delete("content-type"); - init.headers = headers; } - return { init, url: nextUrl }; + init.headers = headers; + return { init, url: nextUrl.toString() }; } function isRedirectStatus(status: number) { diff --git a/test/request.test.ts b/test/request.test.ts index c68e1c0..ac7067e 100644 --- a/test/request.test.ts +++ b/test/request.test.ts @@ -13,6 +13,7 @@ import { buildTopUpTransactionRequest, isSessionInvalidationResponse, parseRequestArgs, + redirectRequest, resolvePaymentIdentity, runRequest, sessionChallengeFromHeader, @@ -177,6 +178,52 @@ describe("request command", () => { expect(stdout.text()).toBe("target"); }); + it("strips credentials on cross-origin redirects", () => { + const redirected = redirectRequest( + { + init: { + headers: { + Authorization: "Bearer secret-token", + Cookie: "session=secret-cookie", + "Proxy-Authorization": "Basic proxy-secret", + }, + method: "GET", + }, + url: "https://api.example.com/redirect", + }, + 302, + "https://other.example.com/target", + ); + const headers = new Headers(redirected.init.headers); + + expect(headers.get("authorization")).toBeNull(); + expect(headers.get("cookie")).toBeNull(); + expect(headers.get("proxy-authorization")).toBeNull(); + }); + + it("preserves credentials on same-origin redirects", () => { + const redirected = redirectRequest( + { + init: { + headers: { + Authorization: "Bearer secret-token", + Cookie: "session=secret-cookie", + "Proxy-Authorization": "Basic proxy-secret", + }, + method: "GET", + }, + url: "https://api.example.com/redirect", + }, + 302, + "/target", + ); + const headers = new Headers(redirected.init.headers); + + expect(headers.get("authorization")).toBe("Bearer secret-token"); + expect(headers.get("cookie")).toBe("session=secret-cookie"); + expect(headers.get("proxy-authorization")).toBe("Basic proxy-secret"); + }); + it("fails when the redirect limit is exceeded", async () => { const server = await testServer((_request, response) => { response.statusCode = 302;