mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-22 14:22:16 +10:00
refactor: ponytail audit
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { isIP } from "node:net";
|
||||
import { BlockList, isIP } from "node:net";
|
||||
|
||||
function normalizeHostname(hostname: string) {
|
||||
return hostname.trim().toLowerCase();
|
||||
@@ -8,170 +8,62 @@ function stripIpv6Brackets(hostname: string): string {
|
||||
return hostname.replace(/^\[/, "").replace(/\]$/, "");
|
||||
}
|
||||
|
||||
function normalizeIpv4MappedIpv6(hostname: string) {
|
||||
const normalized = stripIpv6Brackets(normalizeHostname(hostname));
|
||||
const mapped = normalized.match(/^::ffff:(?<address>.+)$/)?.groups?.address;
|
||||
if (!mapped) return normalized;
|
||||
|
||||
if (isIP(mapped) === 4) return mapped;
|
||||
|
||||
const hexMatch = mapped.match(/^(?<high>[0-9a-f]{1,4}):(?<low>[0-9a-f]{1,4})$/);
|
||||
if (!hexMatch?.groups) return normalized;
|
||||
|
||||
const { high: highHex, low: lowHex } = hexMatch.groups;
|
||||
if (!highHex || !lowHex) return normalized;
|
||||
|
||||
const high = Number.parseInt(highHex, 16);
|
||||
const low = Number.parseInt(lowHex, 16);
|
||||
if (Number.isNaN(high) || Number.isNaN(low) || high > 0xffff || low > 0xffff) return normalized;
|
||||
|
||||
return [high >> 8, high & 0xff, low >> 8, low & 0xff].join(".");
|
||||
}
|
||||
|
||||
function isIpv4MappedIpv6(hostname: string) {
|
||||
const normalized = stripIpv6Brackets(normalizeHostname(hostname));
|
||||
|
||||
return normalized.startsWith("::ffff:");
|
||||
}
|
||||
|
||||
const blockedIpv4Cidrs: Array<[number, number]> = [
|
||||
[knownIpv4ToNumber("0.0.0.0"), 8],
|
||||
[knownIpv4ToNumber("10.0.0.0"), 8],
|
||||
[knownIpv4ToNumber("100.64.0.0"), 10],
|
||||
[knownIpv4ToNumber("127.0.0.0"), 8],
|
||||
[knownIpv4ToNumber("169.254.0.0"), 16],
|
||||
[knownIpv4ToNumber("172.16.0.0"), 12],
|
||||
[knownIpv4ToNumber("192.0.0.0"), 24],
|
||||
[knownIpv4ToNumber("192.0.2.0"), 24],
|
||||
[knownIpv4ToNumber("192.88.99.0"), 24],
|
||||
[knownIpv4ToNumber("192.168.0.0"), 16],
|
||||
[knownIpv4ToNumber("198.18.0.0"), 15],
|
||||
[knownIpv4ToNumber("198.51.100.0"), 24],
|
||||
[knownIpv4ToNumber("203.0.113.0"), 24],
|
||||
[knownIpv4ToNumber("224.0.0.0"), 4],
|
||||
[knownIpv4ToNumber("240.0.0.0"), 4],
|
||||
const blockedIpv4Cidrs: Array<[string, number]> = [
|
||||
["0.0.0.0", 8],
|
||||
["10.0.0.0", 8],
|
||||
["100.64.0.0", 10],
|
||||
["127.0.0.0", 8],
|
||||
["169.254.0.0", 16],
|
||||
["172.16.0.0", 12],
|
||||
["192.0.0.0", 24],
|
||||
["192.0.2.0", 24],
|
||||
["192.88.99.0", 24],
|
||||
["192.168.0.0", 16],
|
||||
["198.18.0.0", 15],
|
||||
["198.51.100.0", 24],
|
||||
["203.0.113.0", 24],
|
||||
["224.0.0.0", 4],
|
||||
["240.0.0.0", 4],
|
||||
];
|
||||
|
||||
const blockedIpv6Cidrs: Array<[bigint, number]> = [
|
||||
[knownIpv6ToBigInt("::"), 128],
|
||||
[knownIpv6ToBigInt("::1"), 128],
|
||||
[knownIpv6ToBigInt("::ffff:0:0"), 96],
|
||||
[knownIpv6ToBigInt("64:ff9b::"), 96],
|
||||
[knownIpv6ToBigInt("64:ff9b:1::"), 48],
|
||||
[knownIpv6ToBigInt("100::"), 64],
|
||||
[knownIpv6ToBigInt("100:0:0:1::"), 64],
|
||||
[knownIpv6ToBigInt("2001::"), 23],
|
||||
[knownIpv6ToBigInt("2001:2::"), 48],
|
||||
[knownIpv6ToBigInt("2001:10::"), 28],
|
||||
[knownIpv6ToBigInt("2001:db8::"), 32],
|
||||
[knownIpv6ToBigInt("2002::"), 16],
|
||||
[knownIpv6ToBigInt("3fff::"), 20],
|
||||
[knownIpv6ToBigInt("5f00::"), 16],
|
||||
[knownIpv6ToBigInt("fc00::"), 7],
|
||||
[knownIpv6ToBigInt("fe80::"), 10],
|
||||
[knownIpv6ToBigInt("ff00::"), 8],
|
||||
const blockedIpv6Cidrs: Array<[string, number]> = [
|
||||
["::", 128],
|
||||
["::1", 128],
|
||||
["::ffff:0:0", 96],
|
||||
["64:ff9b::", 96],
|
||||
["64:ff9b:1::", 48],
|
||||
["100::", 64],
|
||||
["100:0:0:1::", 64],
|
||||
["2001::", 23],
|
||||
["2001:2::", 48],
|
||||
["2001:10::", 28],
|
||||
["2001:db8::", 32],
|
||||
["2002::", 16],
|
||||
["3fff::", 20],
|
||||
["5f00::", 16],
|
||||
["fc00::", 7],
|
||||
["fe80::", 10],
|
||||
["ff00::", 8],
|
||||
];
|
||||
|
||||
function ipv4ToNumber(hostname: string) {
|
||||
const octets = hostname.split(".").map((part) => Number.parseInt(part, 10));
|
||||
if (octets.length !== 4 || octets.some((octet) => Number.isNaN(octet) || octet < 0 || octet > 255)) return null;
|
||||
const blockedIpv4s = new BlockList();
|
||||
for (const [address, prefix] of blockedIpv4Cidrs) blockedIpv4s.addSubnet(address, prefix, "ipv4");
|
||||
|
||||
return (((octets[0] ?? 0) << 24) | ((octets[1] ?? 0) << 16) | ((octets[2] ?? 0) << 8) | (octets[3] ?? 0)) >>> 0;
|
||||
}
|
||||
|
||||
function knownIpv4ToNumber(hostname: string) {
|
||||
const value = ipv4ToNumber(hostname);
|
||||
if (value === null) throw new Error(`Invalid IPv4 CIDR base: ${hostname}`);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function isIpv4InCidr(address: number, base: number, prefix: number) {
|
||||
const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
|
||||
|
||||
return (address & mask) === (base & mask);
|
||||
}
|
||||
|
||||
function expandIpv6(hostname: string) {
|
||||
const normalized = stripIpv6Brackets(normalizeHostname(hostname));
|
||||
const [head = "", tail = ""] = normalized.split("::", 2);
|
||||
const headParts = head ? head.split(":") : [];
|
||||
const tailParts = tail ? tail.split(":") : [];
|
||||
const missing = 8 - headParts.length - tailParts.length;
|
||||
if (missing < 0) return null;
|
||||
|
||||
const parts = [...headParts, ...Array.from({ length: missing }, () => "0"), ...tailParts];
|
||||
if (parts.length !== 8) return null;
|
||||
|
||||
const hextets = parts.map((part) => {
|
||||
if (!/^[0-9a-f]{1,4}$/.test(part)) return null;
|
||||
return Number.parseInt(part, 16);
|
||||
});
|
||||
|
||||
return hextets.every((part) => part !== null && part >= 0 && part <= 0xffff) ? (hextets as number[]) : null;
|
||||
}
|
||||
|
||||
function ipv6ToBigInt(hostname: string) {
|
||||
const hextets = expandIpv6(hostname);
|
||||
if (!hextets) return null;
|
||||
|
||||
return hextets.reduce((value, hextet) => (value << 16n) | BigInt(hextet), 0n);
|
||||
}
|
||||
|
||||
function knownIpv6ToBigInt(hostname: string) {
|
||||
const value = ipv6ToBigInt(hostname);
|
||||
if (value === null) throw new Error(`Invalid IPv6 CIDR base: ${hostname}`);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function isIpv6InCidr(address: bigint, base: bigint, prefix: number) {
|
||||
const bits = 128n;
|
||||
const hostBits = bits - BigInt(prefix);
|
||||
const mask = prefix === 0 ? 0n : ((1n << bits) - 1n) ^ ((1n << hostBits) - 1n);
|
||||
|
||||
return (address & mask) === (base & mask);
|
||||
}
|
||||
|
||||
function isLoopbackOrLocalHostname(hostname: string) {
|
||||
const normalized = normalizeHostname(hostname);
|
||||
return (
|
||||
normalized === "localhost" || normalized === "::1" || normalized === "[::1]" || normalized.endsWith(".localhost")
|
||||
);
|
||||
}
|
||||
|
||||
function isPrivateIPv4(hostname: string) {
|
||||
const address = ipv4ToNumber(hostname);
|
||||
if (address === null) return false;
|
||||
|
||||
return blockedIpv4Cidrs.some(([base, prefix]) => isIpv4InCidr(address, base ?? 0, prefix));
|
||||
}
|
||||
|
||||
function isPrivateIPv6(hostname: string) {
|
||||
const address = ipv6ToBigInt(hostname);
|
||||
if (address === null) return false;
|
||||
|
||||
return blockedIpv6Cidrs.some(([base, prefix]) => isIpv6InCidr(address, base, prefix));
|
||||
}
|
||||
const blockedIpv6s = new BlockList();
|
||||
for (const [address, prefix] of blockedIpv6Cidrs) blockedIpv6s.addSubnet(address, prefix, "ipv6");
|
||||
|
||||
export function isPrivateOrLoopbackHost(hostname: string) {
|
||||
if (isIpv4MappedIpv6(hostname)) return true;
|
||||
|
||||
const normalized = normalizeIpv4MappedIpv6(hostname);
|
||||
if (isLoopbackOrLocalHostname(normalized)) return true;
|
||||
const normalized = stripIpv6Brackets(normalizeHostname(hostname));
|
||||
if (normalized.startsWith("::ffff:")) return true;
|
||||
if (normalized === "localhost" || normalized === "::1" || normalized.endsWith(".localhost")) return true;
|
||||
|
||||
const ipVersion = isIP(normalized);
|
||||
if (ipVersion === 4) return isPrivateIPv4(normalized);
|
||||
if (ipVersion === 6) return isPrivateIPv6(normalized);
|
||||
if (ipVersion === 4) return blockedIpv4s.check(normalized, "ipv4");
|
||||
if (ipVersion === 6) return blockedIpv6s.check(normalized, "ipv6");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function isOAuthLoopbackRedirectHost(hostname: string) {
|
||||
const normalized = stripIpv6Brackets(normalizeHostname(hostname));
|
||||
return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1";
|
||||
}
|
||||
|
||||
export function parseUrl(input: string) {
|
||||
try {
|
||||
return new URL(input);
|
||||
@@ -192,9 +84,9 @@ export function isAllowedOAuthRedirectUri(input: string, trustedOrigins: string[
|
||||
if (parsed.hash) return false;
|
||||
|
||||
const origin = parsed.origin.toLowerCase();
|
||||
const hostname = normalizeHostname(parsed.hostname);
|
||||
const hostname = stripIpv6Brackets(normalizeHostname(parsed.hostname));
|
||||
|
||||
if (parsed.protocol === "http:") return isOAuthLoopbackRedirectHost(hostname);
|
||||
if (parsed.protocol === "http:") return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
||||
if (parsed.protocol !== "https:") return false;
|
||||
if (isPrivateOrLoopbackHost(hostname)) return false;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user