mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-11 13:35:13 +10:00
a4999c04af
- drop dotenv (Node 24 process.loadEnvFile) and dompurify (only used by dead code) - delete unused ui components/hooks (card, progress, checkbox, use-confirm, use-prompt) - delete dead sanitizeHtml/sanitizeCss, url-security helpers, patch-resume tool, schema/page, createResumePatches, patch-proposal preview builder, fonts fallback helpers - inline single-caller wrappers (flags service, auth getSession, pdf renderer passthrough) - deduplicate template color helpers into shared/color-helpers - unexport 50+ internal-only symbols, remove dead export-map entries - replace hand-rolled unique()/useIsMobile with Set spread and usehooks-ts
203 lines
6.5 KiB
TypeScript
203 lines
6.5 KiB
TypeScript
import { isIP } from "node:net";
|
|
|
|
function normalizeHostname(hostname: string) {
|
|
return hostname.trim().toLowerCase();
|
|
}
|
|
|
|
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 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],
|
|
];
|
|
|
|
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;
|
|
|
|
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));
|
|
}
|
|
|
|
export function isPrivateOrLoopbackHost(hostname: string) {
|
|
if (isIpv4MappedIpv6(hostname)) return true;
|
|
|
|
const normalized = normalizeIpv4MappedIpv6(hostname);
|
|
if (isLoopbackOrLocalHostname(normalized)) return true;
|
|
|
|
const ipVersion = isIP(normalized);
|
|
if (ipVersion === 4) return isPrivateIPv4(normalized);
|
|
if (ipVersion === 6) return isPrivateIPv6(normalized);
|
|
|
|
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);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
type OAuthRedirectUriOptions = {
|
|
allowUnsafe?: boolean;
|
|
};
|
|
|
|
export function isAllowedOAuthRedirectUri(input: string, trustedOrigins: string[], options?: OAuthRedirectUriOptions) {
|
|
const parsed = parseUrl(input);
|
|
if (!parsed) return false;
|
|
if (options?.allowUnsafe) return true;
|
|
if (parsed.username || parsed.password) return false;
|
|
if (parsed.hash) return false;
|
|
|
|
const origin = parsed.origin.toLowerCase();
|
|
const hostname = normalizeHostname(parsed.hostname);
|
|
|
|
if (parsed.protocol === "http:") return isOAuthLoopbackRedirectHost(hostname);
|
|
if (parsed.protocol !== "https:") return false;
|
|
if (isPrivateOrLoopbackHost(hostname)) return false;
|
|
|
|
return trustedOrigins.includes(origin);
|
|
}
|