Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions library/helpers/addIPv4TransitionAddresses.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as t from "tap";
import { addIPv4TransitionAddresses } from "./addIPv4TransitionAddresses";

t.test("it adds transition mechanism addresses for IPv4 entries", async (t) => {
t.same(addIPv4TransitionAddresses(["127.0.0.1"]), [
"127.0.0.1",
"64:ff9b::127.0.0.1/128",
"64:ff9b:1::127.0.0.1/128",
"::127.0.0.1/128",
"2002:7f00:1::/48",
]);
});

t.test("it adjusts the prefix length for an IPv4 CIDR range", async (t) => {
t.same(addIPv4TransitionAddresses(["10.0.0.0/8"]), [
"10.0.0.0/8",
"64:ff9b::10.0.0.0/104",
"64:ff9b:1::10.0.0.0/104",
"::10.0.0.0/104",
"2002:a00:0::/24",
]);
});

t.test("it handles empty array", async (t) => {
t.same(addIPv4TransitionAddresses([]), []);
});

t.test("it handles only IPv6 addresses", async (t) => {
t.same(addIPv4TransitionAddresses(["2001:db8::1", "::1"]), [
"2001:db8::1",
"::1",
]);
});

t.test("it handles a mix of IPv4 and IPv6 addresses", async (t) => {
t.same(addIPv4TransitionAddresses(["2001:db8::1", "127.0.0.1", "::1"]), [
"2001:db8::1",
"127.0.0.1",
"::1",
"64:ff9b::127.0.0.1/128",
"64:ff9b:1::127.0.0.1/128",
"::127.0.0.1/128",
"2002:7f00:1::/48",
]);
});
18 changes: 18 additions & 0 deletions library/helpers/addIPv4TransitionAddresses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { mapIPv4To6to4 } from "./mapIPv4To6to4";
import { mapIPv4WithIPv6Prefix } from "./mapIPv4WithIPv6Prefix";

/**
* Adds IPv4-in-IPv6 transition mechanism versions for all IPv4 addresses in the array.
*/
export function addIPv4TransitionAddresses(ips: string[]): string[] {
const ipv4Addresses = ips.filter((ip) => !ip.includes(":"));

return ips.concat(
ipv4Addresses.flatMap((ip) => [
mapIPv4WithIPv6Prefix(ip, "64:ff9b::", 96), // Well-Known NAT64 prefix (RFC 6052)
mapIPv4WithIPv6Prefix(ip, "64:ff9b:1::", 96), // Local-Use NAT64 prefix (RFC 8215)
mapIPv4WithIPv6Prefix(ip, "::", 96), // IPv4-compatible IPv6 address (RFC 4291)
mapIPv4To6to4(ip), // 6to4 (RFC 3056)
])
);
}
21 changes: 21 additions & 0 deletions library/helpers/mapIPv4To6to4.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as t from "tap";
import { mapIPv4To6to4 } from "./mapIPv4To6to4";

t.test("it maps a single IPv4 address to its 6to4 form", async (t) => {
t.equal(mapIPv4To6to4("127.0.0.1"), "2002:7f00:1::/48");
t.equal(mapIPv4To6to4("10.0.0.1"), "2002:a00:1::/48");
t.equal(mapIPv4To6to4("192.168.0.1"), "2002:c0a8:1::/48");
t.equal(mapIPv4To6to4("8.8.8.8"), "2002:808:808::/48");
});

t.test("it adjusts the prefix length for an IPv4 CIDR range", async (t) => {
t.equal(mapIPv4To6to4("10.0.0.0/8"), "2002:a00:0::/24");
t.equal(mapIPv4To6to4("192.168.0.0/16"), "2002:c0a8:0::/32");
});

t.test("it does not pad hex groups that have leading zeros", async (t) => {
// JS drops leading zeros when converting to hex (e.g. 1 -> "1", not "0001"),
// which is still valid, unambiguous IPv6 group notation.
t.equal(mapIPv4To6to4("0.1.0.1"), "2002:1:1::/48");
t.equal(mapIPv4To6to4("0.0.0.0/0"), "2002:0:0::/16");
});
15 changes: 15 additions & 0 deletions library/helpers/mapIPv4To6to4.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { splitCIDR } from "./splitCIDR";

/**
* Maps an IPv4 address to its 6to4 (RFC 3056) IPv6 representation.
* 6to4 embeds the IPv4 address as two hex groups right after the 2002:: prefix.
* e.g. mapIPv4To6to4("10.0.0.1") -> "2002:a00:1::/128"
* e.g. mapIPv4To6to4("10.0.0.0/8") -> "2002:a00:0::/24"
*/
export function mapIPv4To6to4(ip: string): string {
const [address, suffix] = splitCIDR(ip);
const octets = address.split(".").map(Number);
const first = ((octets[0] << 8) | octets[1]).toString(16);
const second = ((octets[2] << 8) | octets[3]).toString(16);
return `2002:${first}:${second}::/${suffix + 16}`;
}
21 changes: 21 additions & 0 deletions library/helpers/mapIPv4WithIPv6Prefix.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as t from "tap";
import { mapIPv4WithIPv6Prefix } from "./mapIPv4WithIPv6Prefix";

t.test("it embeds a single IPv4 address behind a prefix", async (t) => {
t.equal(
mapIPv4WithIPv6Prefix("127.0.0.1", "64:ff9b::", 96),
"64:ff9b::127.0.0.1/128"
);
t.equal(mapIPv4WithIPv6Prefix("10.0.0.1", "::", 96), "::10.0.0.1/128");
});

t.test("it adjusts the prefix length for an IPv4 CIDR range", async (t) => {
t.equal(
mapIPv4WithIPv6Prefix("10.0.0.0/8", "64:ff9b::", 96),
"64:ff9b::10.0.0.0/104"
);
t.equal(
mapIPv4WithIPv6Prefix("192.168.0.0/16", "64:ff9b:1::", 96),
"64:ff9b:1::192.168.0.0/112"
);
});
15 changes: 15 additions & 0 deletions library/helpers/mapIPv4WithIPv6Prefix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { splitCIDR } from "./splitCIDR";

/**
* Embeds an IPv4 address as the trailing bits of a fixed-length IPv6 prefix.
* Used for transition mechanisms that place the IPv4 address at the end of the
* address, e.g. NAT64 (64:ff9b::/96) and IPv4-compatible addresses (::/96).
*/
export function mapIPv4WithIPv6Prefix(
ip: string,
prefix: string,
prefixBitLength: number
): string {
const [address, suffix] = splitCIDR(ip);
return `${prefix}${address}/${suffix + prefixBitLength}`;
}
23 changes: 23 additions & 0 deletions library/helpers/splitCIDR.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as t from "tap";
import { splitCIDR } from "./splitCIDR";

t.test("it splits an IP with a CIDR suffix", async (t) => {
t.same(splitCIDR("10.0.0.0/8"), ["10.0.0.0", 8]);
t.same(splitCIDR("192.168.0.0/16"), ["192.168.0.0", 16]);
t.same(splitCIDR("2001:db8::/32"), ["2001:db8::", 32]);
});

t.test(
"it defaults to a single-host prefix when no suffix is present",
async (t) => {
t.same(splitCIDR("127.0.0.1"), ["127.0.0.1", 32]);
t.same(splitCIDR("::1"), ["::1", 128]);
}
);

t.test("it handles boundary suffix values", async (t) => {
t.same(splitCIDR("0.0.0.0/0"), ["0.0.0.0", 0]);
t.same(splitCIDR("::/0"), ["::", 0]);
t.same(splitCIDR("127.0.0.1/32"), ["127.0.0.1", 32]);
t.same(splitCIDR("::1/128"), ["::1", 128]);
});
12 changes: 12 additions & 0 deletions library/helpers/splitCIDR.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Splits an IP address into its address part and prefix length.
* e.g. "10.0.0.0/8" -> ["10.0.0.0", 8], "127.0.0.1" -> ["127.0.0.1", 32]
* e.g. "::1" -> ["::1", 128]
*/
export function splitCIDR(ip: string): [string, number] {
if (!ip.includes("/")) {
return [ip, ip.includes(":") ? 128 : 32];
}
const [address, suffix] = ip.split("/");
return [address, Number.parseInt(suffix, 10)];
}
34 changes: 34 additions & 0 deletions library/vulnerabilities/ssrf/containsPrivateIPAddress.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ const publicIPs = [
"::ffff:1.2.3.4",
"::ffff:172.1.2.3",
"::ffff:192.145.0.0",
"64:ff9b::8.8.8.8",
"64:ff9b:1::8.8.8.8",
"2002:808:808::",
"::8.8.8.8",

// NAT64-encoded addresses just outside the 10.0.0.0/8 private range
"64:ff9b::9ff:ffff", // 9.255.255.255
"64:ff9b::b00:0", // 11.0.0.0
];

const privateIPs = [
Expand Down Expand Up @@ -163,6 +171,32 @@ const privateIPs = [

// Alibaba Cloud metadata service
"100.100.100.200",

// NAT64 (RFC 6052) encoding private IPv4 addresses
"64:ff9b::7f00:1", // 127.0.0.1
"64:ff9b::a00:0", // 10.0.0.0 (start of 10.0.0.0/8)
"64:ff9b::a00:1", // 10.0.0.1
"64:ff9b::a00:203", // 10.0.2.3 (inside 10.0.0.0/8, not the network address)
"64:ff9b::c0a8:1", // 192.168.0.1
"64:ff9b::a9fe:a9fe", // 169.254.169.254 (cloud metadata)

// NAT64 Local-Use prefix (RFC 8215) encoding private IPv4 addresses
"64:ff9b:1::7f00:1", // 127.0.0.1
"64:ff9b:1::a00:1", // 10.0.0.1
"64:ff9b:1::c0a8:1", // 192.168.0.1
"64:ff9b:1::a9fe:a9fe", // 169.254.169.254 (cloud metadata)

// 6to4 (RFC 3056) encoding private IPv4 addresses
"2002:7f00:1::", // 127.0.0.1
"2002:a00:1::", // 10.0.0.1
"2002:c0a8:1::", // 192.168.0.1
"2002:a9fe:a9fe::", // 169.254.169.254 (cloud metadata)

// IPv4-compatible IPv6 addresses (RFC 4291) encoding private IPv4 addresses
"::7f00:1", // 127.0.0.1
"::a00:1", // 10.0.0.1
"::c0a8:1", // 192.168.0.1
"::a9fe:a9fe", // 169.254.169.254 (cloud metadata)
];

const invalidIPs = [
Expand Down
2 changes: 2 additions & 0 deletions library/vulnerabilities/ssrf/isPrivateIP.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { addIPv4MappedAddresses } from "../../helpers/addIPv4MappedAddresses";
import { addIPv4TransitionAddresses } from "../../helpers/addIPv4TransitionAddresses";
import { IPMatcher } from "../../helpers/ip-matcher/IPMatcher";

const PRIVATE_IP_RANGES = [
Expand Down Expand Up @@ -36,6 +37,7 @@ const PRIVATE_IPV6_RANGES = [
// Small list, frequently accessed: add IPv4-mapped versions at creation time for fast lookups
const privateIp = new IPMatcher([
...addIPv4MappedAddresses(PRIVATE_IP_RANGES),
...addIPv4TransitionAddresses(PRIVATE_IP_RANGES),
...PRIVATE_IPV6_RANGES,
]);

Expand Down
Loading