fix(shared): block cloud-metadata hosts under default remote-image guard (#11755)

Boarded in a combined worktree: typecheck:core, check:dashboard-typecheck, check:file-size, check:changelog-integrity, check:complexity, check:cognitive-complexity, check:cycles, check-deps all green; 10/10 focused tests pass. Real SSRF gap confirmed — the default "block-metadata" guard mode fell through to the unchecked parseOutboundUrl() while 3 other call sites of the same guard mode already routed through parseAndValidateNonMetadataUrl(). Good catch that the existing test suite only ever exercised "public-only" explicitly. Retargeted from the stale release/v3.8.50 base to release/v3.8.51. Thanks for closing a real cloud-metadata SSRF exposure.
This commit is contained in:
santosraju99-hub
2026-08-30 12:37:18 +05:30
committed by GitHub
parent 55691e0416
commit faebf6de5f
2 changed files with 53 additions and 1 deletions

View File

@@ -4,6 +4,7 @@ import { Agent, fetch as undiciFetch } from "undici";
import {
type OutboundUrlGuardMode,
isPrivateHost,
parseAndValidateNonMetadataUrl,
parseAndValidatePublicUrl,
parseOutboundUrl,
} from "@/shared/network/outboundUrlGuard";
@@ -51,7 +52,9 @@ export type RemoteMediaFetchOptions = RemoteImageFetchOptions;
export type RemoteMediaFetchResult = RemoteImageFetchResult;
function validateRemoteImageUrl(input: string | URL, guard: OutboundUrlGuardMode) {
return guard === "public-only" ? parseAndValidatePublicUrl(input) : parseOutboundUrl(input);
if (guard === "public-only") return parseAndValidatePublicUrl(input);
if (guard === "block-metadata") return parseAndValidateNonMetadataUrl(input);
return parseOutboundUrl(input);
}
function requireHttps(url: URL, enabled: boolean): URL {

View File

@@ -56,3 +56,52 @@ test("fetchRemoteImage blocks redirects to private image hosts", async () => {
/Blocked private or local provider URL/
);
});
// The default guard mode (no `guard` option passed, matching production callers that rely on
// `getProviderOutboundGuard()`'s local-first default) is "block-metadata". Every other test in
// this file passes `guard: "public-only"` explicitly, which never exercised this branch — the
// gap that let `validateRemoteImageUrl()`'s fall-through to the unchecked `parseOutboundUrl()`
// for cloud-metadata hosts go undetected.
test("fetchRemoteImage blocks cloud-metadata hosts under the default block-metadata guard", async () => {
let called = false;
await assert.rejects(
() =>
fetchRemoteImage("http://169.254.169.254/latest/meta-data", {
fetchImpl: async () => {
called = true;
return new Response("unexpected");
},
}),
/Blocked cloud-metadata endpoint/
);
assert.equal(called, false);
});
test("fetchRemoteImage allows private/LAN image hosts under the default block-metadata guard", async () => {
const result = await fetchRemoteImage("http://192.168.1.50:8080/local.png", {
fetchImpl: async () =>
new Response(new Uint8Array([1, 2, 3]), {
status: 200,
headers: { "content-type": "image/png" },
}),
});
assert.equal(result.buffer.toString("base64"), "AQID");
});
test("fetchRemoteImage blocks redirects to cloud-metadata hosts under the default block-metadata guard", async () => {
await assert.rejects(
() =>
fetchRemoteImage("https://cdn.example.com/redirect.png", {
fetchImpl: async () =>
new Response(null, {
status: 302,
headers: { location: "http://169.254.169.254/latest/meta-data" },
}),
lookup: publicLookup,
}),
/Blocked cloud-metadata endpoint/
);
});