From 31a28eb5ba6652245945946200d87dc71bbdc69e Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 14 May 2026 16:16:43 +0800 Subject: [PATCH] fix(media): reject malformed redirect locations --- CHANGELOG.md | 1 + src/media/store.redirect.test.ts | 9 +++++++++ src/media/store.ts | 14 +++++++++++--- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72dd938aedc..6e86487ee5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai - Security/sandbox: include Windows `USERPROFILE` in the sandbox blocked home roots so credential-bearing binds (such as `.codex`, `.openclaw`, or `.ssh` under the Windows user profile) are denied even when `HOME` points at a different shell home. (#63074) Thanks @luoyanglang. - Gateway/OpenAI-compatible HTTP: parse shared JSON endpoint paths without trusting malformed Host headers, avoiding 500s before `/v1/chat/completions`, `/v1/responses`, and `/v1/embeddings` request handling. - Voice-call webhooks: parse webhook and realtime upgrade paths without trusting malformed Host headers, avoiding 500s before provider signature checks or path rejection. +- Media store: reject malformed redirect `Location` headers as media-download failures instead of letting URL parsing escape the async response callback. - Models config/auth: stop inferring provider env-var markers from broad `^[A-Z_][A-Z0-9_]*$` strings, and resolve config-backed provider `apiKey` values only through structured env SecretRefs (`secrets.providers[id]` / `secrets.defaults`), so unrelated env vars cannot accidentally become provider credentials. Thanks @sallyom. - Media fetch: skip allocating and buffering the response body for bodyless media responses (HEAD probes and 204-style empty bodies), avoiding wasted heap on streams that carry no payload. Thanks @shakkernerd. - CLI/onboarding: forward provider-specific auth flags (e.g. `--openai-api-key`) through the onboarding wizard so they reach provider auth methods via `ctx.opts`, letting `--openai-api-key "$OPENAI_API_KEY"` skip the redundant "use existing env var?" prompt in non-interactive harnesses. (#81669) Thanks @sjf. diff --git a/src/media/store.redirect.test.ts b/src/media/store.redirect.test.ts index 358efbecea3..9f23a7b5605 100644 --- a/src/media/store.redirect.test.ts +++ b/src/media/store.redirect.test.ts @@ -217,4 +217,13 @@ describe("media store redirects", () => { }); await expectRedirectSaveFailure("Redirect loop or missing Location header"); }); + + it("fails when redirect location is malformed", async () => { + mockRequest.mockImplementationOnce((_url, _opts, cb) => { + const exchange = mockRedirectExchange({ location: "http://[" }); + exchange.send(cb); + return exchange.req; + }); + await expectRedirectSaveFailure("Invalid redirect Location header"); + }); }); diff --git a/src/media/store.ts b/src/media/store.ts index 0487741e553..7611c61d576 100644 --- a/src/media/store.ts +++ b/src/media/store.ts @@ -230,12 +230,20 @@ async function downloadToFile( reject(new Error(`Redirect loop or missing Location header`)); return; } - const redirectUrl = new URL(location, url).href; + let redirectUrl: URL; + try { + redirectUrl = new URL(location, url); + } catch { + reject(new Error("Invalid redirect Location header")); + return; + } const redirectHeaders = - new URL(redirectUrl).origin === parsedUrl.origin + redirectUrl.origin === parsedUrl.origin ? headers : retainSafeHeadersForCrossOriginRedirect(headers); - resolve(downloadToFile(redirectUrl, dest, redirectHeaders, maxRedirects - 1, maxBytes)); + resolve( + downloadToFile(redirectUrl.href, dest, redirectHeaders, maxRedirects - 1, maxBytes), + ); return; } if (!res.statusCode || res.statusCode >= 400) {