fix: allow RFC2544 benchmark range (198.18.0.0/15) through SSRF filter
Telegram's API and file servers resolve to IPs in the 198.18.0.0/15 range (RFC 2544 benchmarking range). The SSRF filter was blocking these addresses because ipaddr.js classifies them as 'reserved', and the filter also had an explicit RFC2544_BENCHMARK_PREFIX check that blocked them unconditionally. Fix: exempt 198.18.0.0/15 from the 'reserved' range block in isBlockedSpecialUseIpv4Address(). Other 'reserved' ranges (TEST-NET-2, TEST-NET-3, documentation prefixes) remain blocked. The explicit RFC2544_BENCHMARK_PREFIX check is repurposed as the exemption guard. Closes #24973
This commit is contained in:
@@ -37,13 +37,27 @@ describe("fetchWithSsrFGuard hardening", () => {
|
||||
const fetchImpl = vi.fn();
|
||||
await expect(
|
||||
fetchWithSsrFGuard({
|
||||
url: "http://198.18.0.1:8080/internal",
|
||||
url: "http://198.51.100.1:8080/internal",
|
||||
fetchImpl,
|
||||
}),
|
||||
).rejects.toThrow(/private|internal|blocked/i);
|
||||
expect(fetchImpl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows RFC2544 benchmark range IPv4 literal URLs (Telegram)", async () => {
|
||||
const lookupFn = vi.fn(async () => [
|
||||
{ address: "198.18.0.153", family: 4 },
|
||||
]) as unknown as LookupFn;
|
||||
const fetchImpl = vi.fn().mockResolvedValueOnce(new Response("ok", { status: 200 }));
|
||||
// Should not throw — 198.18.x.x is allowed now
|
||||
const result = await fetchWithSsrFGuard({
|
||||
url: "http://198.18.0.153/file",
|
||||
fetchImpl,
|
||||
lookupFn,
|
||||
});
|
||||
expect(result.response.status).toBe(200);
|
||||
});
|
||||
|
||||
it("blocks redirect chains that hop to private hosts", async () => {
|
||||
const lookupFn = vi.fn(async () => [
|
||||
{ address: "93.184.216.34", family: 4 },
|
||||
|
||||
@@ -51,12 +51,20 @@ describe("ssrf pinning", () => {
|
||||
|
||||
it.each([
|
||||
{ name: "RFC1918 private address", address: "10.0.0.8" },
|
||||
{ name: "RFC2544 benchmarking range", address: "198.18.0.1" },
|
||||
{ name: "TEST-NET-2 reserved range", address: "198.51.100.1" },
|
||||
])("rejects blocked DNS results: $name", async ({ address }) => {
|
||||
const lookup = vi.fn(async () => [{ address, family: 4 }]) as unknown as LookupFn;
|
||||
await expect(resolvePinnedHostname("example.com", lookup)).rejects.toThrow(/private|internal/i);
|
||||
});
|
||||
|
||||
it("allows RFC2544 benchmark range addresses (used by Telegram)", async () => {
|
||||
const lookup = vi.fn(async () => [
|
||||
{ address: "198.18.0.153", family: 4 },
|
||||
]) as unknown as LookupFn;
|
||||
const pinned = await resolvePinnedHostname("api.telegram.org", lookup);
|
||||
expect(pinned.addresses).toContain("198.18.0.153");
|
||||
});
|
||||
|
||||
it("falls back for non-matching hostnames", async () => {
|
||||
const fallback = vi.fn((host: string, options?: unknown, callback?: unknown) => {
|
||||
const cb = typeof options === "function" ? options : (callback as () => void);
|
||||
|
||||
@@ -3,8 +3,6 @@ import { normalizeFingerprint } from "../tls/fingerprint.js";
|
||||
import { isBlockedHostnameOrIp, isPrivateIpAddress } from "./ssrf.js";
|
||||
|
||||
const privateIpCases = [
|
||||
"198.18.0.1",
|
||||
"198.19.255.254",
|
||||
"198.51.100.42",
|
||||
"203.0.113.10",
|
||||
"192.0.0.8",
|
||||
@@ -15,7 +13,6 @@ const privateIpCases = [
|
||||
"240.0.0.1",
|
||||
"255.255.255.255",
|
||||
"::ffff:127.0.0.1",
|
||||
"::ffff:198.18.0.1",
|
||||
"64:ff9b::198.51.100.42",
|
||||
"0:0:0:0:0:ffff:7f00:1",
|
||||
"0000:0000:0000:0000:0000:ffff:7f00:0001",
|
||||
@@ -32,7 +29,6 @@ const privateIpCases = [
|
||||
"2002:a9fe:a9fe::",
|
||||
"2001:0000:0:0:0:0:80ff:fefe",
|
||||
"2001:0000:0:0:0:0:3f57:fefe",
|
||||
"2002:c612:0001::",
|
||||
"::",
|
||||
"::1",
|
||||
"fe80::1%lo0",
|
||||
@@ -45,13 +41,18 @@ const privateIpCases = [
|
||||
const publicIpCases = [
|
||||
"93.184.216.34",
|
||||
"198.17.255.255",
|
||||
"198.18.0.1",
|
||||
"198.18.0.153",
|
||||
"198.19.255.254",
|
||||
"198.20.0.1",
|
||||
"2002:c612:0001::",
|
||||
"198.51.99.1",
|
||||
"198.51.101.1",
|
||||
"203.0.112.1",
|
||||
"203.0.114.1",
|
||||
"223.255.255.255",
|
||||
"2606:4700:4700::1111",
|
||||
"::ffff:198.18.0.1",
|
||||
"2001:db8::1",
|
||||
"64:ff9b::8.8.8.8",
|
||||
"64:ff9b:1::8.8.8.8",
|
||||
@@ -119,9 +120,13 @@ describe("isBlockedHostnameOrIp", () => {
|
||||
expect(isBlockedHostnameOrIp("2001:db8::1")).toBe(false);
|
||||
});
|
||||
|
||||
it("blocks IPv4 special-use ranges but allows adjacent public ranges", () => {
|
||||
expect(isBlockedHostnameOrIp("198.18.0.1")).toBe(true);
|
||||
it("allows RFC2544 benchmark range (used by Telegram) but blocks adjacent special-use ranges", () => {
|
||||
expect(isBlockedHostnameOrIp("198.18.0.1")).toBe(false);
|
||||
expect(isBlockedHostnameOrIp("198.18.0.153")).toBe(false);
|
||||
expect(isBlockedHostnameOrIp("198.19.255.254")).toBe(false);
|
||||
expect(isBlockedHostnameOrIp("198.20.0.1")).toBe(false);
|
||||
expect(isBlockedHostnameOrIp("198.51.100.1")).toBe(true);
|
||||
expect(isBlockedHostnameOrIp("203.0.113.1")).toBe(true);
|
||||
});
|
||||
|
||||
it("blocks legacy IPv4 literal representations", () => {
|
||||
|
||||
@@ -28,6 +28,12 @@ const PRIVATE_OR_LOOPBACK_IPV6_RANGES = new Set<Ipv6Range>([
|
||||
"linkLocal",
|
||||
"uniqueLocal",
|
||||
]);
|
||||
/**
|
||||
* RFC 2544 benchmark range (198.18.0.0/15). Originally reserved for network
|
||||
* device benchmarking, but in practice used by real services — notably
|
||||
* Telegram's API/file servers resolve to addresses in this block. We
|
||||
* therefore exempt it from the SSRF block list.
|
||||
*/
|
||||
const RFC2544_BENCHMARK_PREFIX: [ipaddr.IPv4, number] = [ipaddr.IPv4.parse("198.18.0.0"), 15];
|
||||
|
||||
const EMBEDDED_IPV4_SENTINEL_RULES: Array<{
|
||||
@@ -248,9 +254,13 @@ export function isCarrierGradeNatIpv4Address(raw: string | undefined): boolean {
|
||||
}
|
||||
|
||||
export function isBlockedSpecialUseIpv4Address(address: ipaddr.IPv4): boolean {
|
||||
return (
|
||||
BLOCKED_IPV4_SPECIAL_USE_RANGES.has(address.range()) || address.match(RFC2544_BENCHMARK_PREFIX)
|
||||
);
|
||||
const range = address.range();
|
||||
if (range === "reserved" && address.match(RFC2544_BENCHMARK_PREFIX)) {
|
||||
// 198.18.0.0/15 is classified as "reserved" by ipaddr.js but is used by
|
||||
// real public services (e.g. Telegram API). Allow it through.
|
||||
return false;
|
||||
}
|
||||
return BLOCKED_IPV4_SPECIAL_USE_RANGES.has(range);
|
||||
}
|
||||
|
||||
function decodeIpv4FromHextets(high: number, low: number): ipaddr.IPv4 {
|
||||
|
||||
Reference in New Issue
Block a user