Files
Termix/src/backend/tests/utils/safe-outbound-fetch.test.ts
T

136 lines
4.5 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import type { LookupAddress, LookupAllOptions, LookupOptions } from "dns";
import {
createDnsLookupHook,
isBlockedAddress,
} from "../../utils/safe-outbound-fetch.js";
describe("isBlockedAddress", () => {
it("allows public IPv4 addresses", () => {
expect(isBlockedAddress("8.8.8.8")).toBe(false);
expect(isBlockedAddress("104.21.52.150")).toBe(false);
});
it("blocks private/reserved IPv4 ranges", () => {
expect(isBlockedAddress("10.0.0.1")).toBe(true);
expect(isBlockedAddress("172.16.0.1")).toBe(true);
expect(isBlockedAddress("192.168.1.1")).toBe(true);
expect(isBlockedAddress("127.0.0.1")).toBe(true);
expect(isBlockedAddress("169.254.1.1")).toBe(true);
expect(isBlockedAddress("100.64.0.1")).toBe(true);
});
it("allows public IPv6 addresses", () => {
expect(isBlockedAddress("2606:4700:3034::ac43:c88d")).toBe(false);
expect(isBlockedAddress("2001:4860:4860::8888")).toBe(false);
});
it("blocks private/reserved IPv6 ranges", () => {
expect(isBlockedAddress("::1")).toBe(true);
expect(isBlockedAddress("fc00::1")).toBe(true);
expect(isBlockedAddress("fe80::1")).toBe(true);
});
it("blocks IPv4-mapped-IPv6 spoofing of private addresses", () => {
expect(isBlockedAddress("::ffff:127.0.0.1")).toBe(true);
expect(isBlockedAddress("::ffff:192.168.1.1")).toBe(true);
expect(isBlockedAddress("::ffff:10.0.0.1")).toBe(true);
});
it("does not block IPv4-mapped-IPv6 form of public addresses", () => {
expect(isBlockedAddress("::ffff:104.21.52.150")).toBe(false);
expect(isBlockedAddress("::ffff:8.8.8.8")).toBe(false);
});
it("blocks unparseable input", () => {
expect(isBlockedAddress("not-an-ip")).toBe(true);
});
});
// These exercise createDnsLookupHook directly against a fake resolver,
// bypassing fetch()/undici entirely. That's the actual code path the
// original bug lived in — a public IPv4 address getting misclassified as
// private — and testing it through a real Agent/fetch call would only
// add flakiness (real TCP connects, undici's own quirks) without adding
// coverage of the logic that actually broke.
function runHook(
addresses: LookupAddress[],
error: NodeJS.ErrnoException | null = null,
options: LookupOptions = { all: true },
) {
const fakeLookup = (
_host: string,
_opts: LookupAllOptions,
cb: (err: NodeJS.ErrnoException | null, addrs: LookupAddress[]) => void,
) => cb(error, addresses);
const hook = createDnsLookupHook(fakeLookup);
const callback = vi.fn();
hook("example.invalid", options, callback);
return callback;
}
describe("createDnsLookupHook", () => {
it("returns every public address when all addresses are requested", () => {
const addresses = [
{ address: "104.21.52.150", family: 4 },
{ address: "2606:4700:3034::ac43:c88d", family: 6 },
];
const callback = runHook(addresses);
expect(callback).toHaveBeenCalledWith(null, addresses);
});
it.each<LookupOptions>([{ all: false }, {}])(
"returns one public address for single-address lookup options %j",
(options) => {
const callback = runHook(
[{ address: "104.21.52.150", family: 4 }],
null,
options,
);
expect(callback).toHaveBeenCalledWith(null, "104.21.52.150", 4);
},
);
it("rejects a mixed result containing a mapped private address", () => {
const callback = runHook([
{ address: "104.21.52.150", family: 4 },
{ address: "::ffff:127.0.0.1", family: 6 },
]);
expect(callback).toHaveBeenCalledWith(
expect.objectContaining({
message: "Private destinations are not allowed",
}),
[],
);
});
it("rejects a private address with the private-destination error", () => {
const callback = runHook([{ address: "192.168.1.1", family: 4 }]);
expect(callback).toHaveBeenCalledWith(
expect.objectContaining({
message: "Private destinations are not allowed",
}),
[],
);
});
it("rejects with a distinct error when DNS returns no addresses", () => {
const callback = runHook([]);
expect(callback).toHaveBeenCalledWith(
expect.objectContaining({
message: "DNS resolution returned no addresses",
}),
[],
);
});
it("propagates a real DNS lookup error untouched", () => {
const dnsError = Object.assign(new Error("getaddrinfo ENOTFOUND"), {
code: "ENOTFOUND",
});
const callback = runHook([], dnsError);
expect(callback).toHaveBeenCalledWith(dnsError, []);
});
});