mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-29 10:21:34 +00:00
136 lines
4.5 KiB
TypeScript
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, []);
|
|
});
|
|
});
|