refactor: unify SSRF hostname/ip precheck and add policy regression

This commit is contained in:
Peter Steinberger
2026-02-19 10:25:25 +01:00
parent b4792c7362
commit d05c8eb912
2 changed files with 38 additions and 9 deletions

View File

@@ -4,6 +4,7 @@ import {
type LookupFn,
resolvePinnedHostname,
resolvePinnedHostnameWithPolicy,
SsrFBlockedError,
} from "./ssrf.js";
describe("ssrf pinning", () => {
@@ -107,4 +108,34 @@ describe("ssrf pinning", () => {
}),
).rejects.toThrow(/allowlist/i);
});
it("blocks ISATAP embedded private IPv4 before DNS lookup", async () => {
const lookup = vi.fn(async () => [
{ address: "93.184.216.34", family: 4 },
]) as unknown as LookupFn;
await expect(
resolvePinnedHostnameWithPolicy("2001:db8:1234::5efe:127.0.0.1", {
lookupFn: lookup,
}),
).rejects.toThrow(SsrFBlockedError);
expect(lookup).not.toHaveBeenCalled();
});
it("allows ISATAP embedded private IPv4 when private network is explicitly enabled", async () => {
const lookup = vi.fn(async () => [
{ address: "2001:db8:1234::5efe:127.0.0.1", family: 6 },
]) as unknown as LookupFn;
await expect(
resolvePinnedHostnameWithPolicy("2001:db8:1234::5efe:127.0.0.1", {
lookupFn: lookup,
policy: { allowPrivateNetwork: true },
}),
).resolves.toMatchObject({
hostname: "2001:db8:1234::5efe:127.0.0.1",
addresses: ["2001:db8:1234::5efe:127.0.0.1"],
});
expect(lookup).toHaveBeenCalledTimes(1);
});
});

View File

@@ -316,6 +316,10 @@ export function isBlockedHostname(hostname: string): boolean {
if (!normalized) {
return false;
}
return isBlockedHostnameNormalized(normalized);
}
function isBlockedHostnameNormalized(normalized: string): boolean {
if (BLOCKED_HOSTNAMES.has(normalized)) {
return true;
}
@@ -331,7 +335,7 @@ export function isBlockedHostnameOrIp(hostname: string): boolean {
if (!normalized) {
return false;
}
return isBlockedHostname(normalized) || isPrivateIpAddress(normalized);
return isBlockedHostnameNormalized(normalized) || isPrivateIpAddress(normalized);
}
export function createPinnedLookup(params: {
@@ -415,14 +419,8 @@ export async function resolvePinnedHostnameWithPolicy(
throw new SsrFBlockedError(`Blocked hostname (not in allowlist): ${hostname}`);
}
if (!allowPrivateNetwork && !isExplicitAllowed) {
if (isBlockedHostname(normalized)) {
throw new SsrFBlockedError(`Blocked hostname: ${hostname}`);
}
if (isPrivateIpAddress(normalized)) {
throw new SsrFBlockedError("Blocked: private/internal IP address");
}
if (!allowPrivateNetwork && !isExplicitAllowed && isBlockedHostnameOrIp(normalized)) {
throw new SsrFBlockedError("Blocked hostname or private/internal IP address");
}
const lookupFn = params.lookupFn ?? dnsLookup;