fix(media): reject malformed redirect locations

This commit is contained in:
Vincent Koc
2026-05-14 16:16:43 +08:00
parent f71df80522
commit 31a28eb5ba
3 changed files with 21 additions and 3 deletions

View File

@@ -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.

View File

@@ -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");
});
});

View File

@@ -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) {