evalstate's picture
download
raw
7.69 kB
export interface ExternalAddressOptions {
allowDnsRebindMitigation?: boolean;
}
const INTERNAL_ADDRESS_HOST_ALLOWLIST_ENV = 'ALLOW_INTERNAL_ADDRESS_HOSTS';
function normalizeHostname(hostname: string): string {
return hostname.trim().toLowerCase().replace(/\.+$/, '');
}
function getInternalAddressHostAllowlist(): string[] {
const raw = process.env[INTERNAL_ADDRESS_HOST_ALLOWLIST_ENV];
if (!raw) {
return [];
}
return raw
.split(',')
.map((entry) => normalizeHostname(entry))
.filter((entry) => entry.length > 0);
}
function hostnameMatchesPattern(hostname: string, pattern: string): boolean {
if (pattern.startsWith('*.')) {
const baseDomain = pattern.slice(2);
if (!baseDomain) {
return false;
}
return hostname === baseDomain || hostname.endsWith(`.${baseDomain}`);
}
return hostname === pattern;
}
function isInternalAddressAllowedForHostname(hostname: string): boolean {
const normalizedHostname = normalizeHostname(hostname);
if (!normalizedHostname) {
return false;
}
const allowlist = getInternalAddressHostAllowlist();
if (allowlist.length === 0) {
return false;
}
return allowlist.some((pattern) => hostnameMatchesPattern(normalizedHostname, pattern));
}
function normalizeIpLiteral(host: string): string {
if (host.startsWith('[') && host.endsWith(']')) {
return host.slice(1, -1);
}
return host;
}
function parseIpv4ToInt(ip: string): number {
const parts = ip.split('.').map((part) => Number.parseInt(part, 10));
if (parts.length !== 4 || parts.some((part) => Number.isNaN(part) || part < 0 || part > 255)) {
throw new Error(`Invalid IPv4 address: ${ip}`);
}
return parts.reduce((acc, part) => acc * 256 + part, 0);
}
function ipv4InRange(ipValue: number, start: string, end: string): boolean {
const startValue = parseIpv4ToInt(start);
const endValue = parseIpv4ToInt(end);
return ipValue >= startValue && ipValue <= endValue;
}
function isIpv4InternalOrReserved(ip: string): boolean {
const value = parseIpv4ToInt(ip);
return (
ipv4InRange(value, '0.0.0.0', '0.255.255.255') ||
ipv4InRange(value, '10.0.0.0', '10.255.255.255') ||
ipv4InRange(value, '100.64.0.0', '100.127.255.255') ||
ipv4InRange(value, '127.0.0.0', '127.255.255.255') ||
ipv4InRange(value, '169.254.0.0', '169.254.255.255') ||
ipv4InRange(value, '172.16.0.0', '172.31.255.255') ||
ipv4InRange(value, '192.0.0.0', '192.0.0.255') ||
ipv4InRange(value, '192.0.2.0', '192.0.2.255') ||
ipv4InRange(value, '192.88.99.0', '192.88.99.255') ||
ipv4InRange(value, '192.168.0.0', '192.168.255.255') ||
ipv4InRange(value, '198.18.0.0', '198.19.255.255') ||
ipv4InRange(value, '198.51.100.0', '198.51.100.255') ||
ipv4InRange(value, '203.0.113.0', '203.0.113.255') ||
ipv4InRange(value, '224.0.0.0', '239.255.255.255') ||
ipv4InRange(value, '240.0.0.0', '255.255.255.255')
);
}
function parseIpv6ToBigInt(ip: string): bigint {
const zoneIndex = ip.indexOf('%');
const zoneStripped = zoneIndex >= 0 ? ip.slice(0, zoneIndex) : ip;
let working = zoneStripped;
if (working.includes('.')) {
const lastColon = working.lastIndexOf(':');
if (lastColon < 0) {
throw new Error(`Invalid IPv6 address: ${ip}`);
}
const ipv4Part = working.slice(lastColon + 1);
const ipv4Value = parseIpv4ToInt(ipv4Part);
const high = ((ipv4Value >>> 16) & 0xffff).toString(16);
const low = (ipv4Value & 0xffff).toString(16);
working = `${working.slice(0, lastColon)}:${high}:${low}`;
}
const split = working.split('::');
if (split.length > 2) {
throw new Error(`Invalid IPv6 address: ${ip}`);
}
const left = split[0] ? split[0].split(':').filter(Boolean) : [];
const right = split[1] ? split[1].split(':').filter(Boolean) : [];
const missingCount = 8 - (left.length + right.length);
if (split.length === 1 && missingCount !== 0) {
throw new Error(`Invalid IPv6 address: ${ip}`);
}
if (missingCount < 0) {
throw new Error(`Invalid IPv6 address: ${ip}`);
}
const full = [...left, ...Array.from({ length: missingCount }, () => '0'), ...right];
if (full.length !== 8) {
throw new Error(`Invalid IPv6 address: ${ip}`);
}
let value = 0n;
for (const part of full) {
const segment = Number.parseInt(part, 16);
if (Number.isNaN(segment) || segment < 0 || segment > 0xffff) {
throw new Error(`Invalid IPv6 address: ${ip}`);
}
value = (value << 16n) + BigInt(segment);
}
return value;
}
function isIpv6InCidr(ipValue: bigint, prefixValue: bigint, prefixLength: number): boolean {
const hostBits = 128n - BigInt(prefixLength);
const mask = ((1n << BigInt(prefixLength)) - 1n) << hostBits;
return (ipValue & mask) === (prefixValue & mask);
}
function isIpv6InternalOrReserved(ip: string): boolean {
const value = parseIpv6ToBigInt(ip);
if (value === 0n || value === 1n) {
return true;
}
// IPv4-mapped IPv6 ::ffff:a.b.c.d
if (value >> 32n === 0xffffn) {
const ipv4Value = Number(value & 0xffffffffn);
const octet1 = (ipv4Value >>> 24) & 0xff;
const octet2 = (ipv4Value >>> 16) & 0xff;
const octet3 = (ipv4Value >>> 8) & 0xff;
const octet4 = ipv4Value & 0xff;
return isIpv4InternalOrReserved(
`${octet1.toString()}.${octet2.toString()}.${octet3.toString()}.${octet4.toString()}`
);
}
return (
isIpv6InCidr(value, 0xfc00n << 112n, 7) || // Unique local
isIpv6InCidr(value, 0xfe80n << 112n, 10) || // Link-local
isIpv6InCidr(value, 0xff00n << 112n, 8) || // Multicast
isIpv6InCidr(value, 0x20010db8n << 96n, 32) || // Documentation
isIpv6InCidr(value, 0x20010010n << 96n, 28) // ORCHID
);
}
export function isIpInternalOrReserved(ip: string): boolean {
const normalizedIp = normalizeIpLiteral(ip);
const ipVersion = detectIpVersion(normalizedIp);
if (ipVersion === 0) {
throw new Error(`Invalid IP address: ${ip}`);
}
if (ipVersion === 4) {
return isIpv4InternalOrReserved(normalizedIp);
}
return isIpv6InternalOrReserved(normalizedIp);
}
async function lookupAll(hostname: string): Promise<string[]> {
const { lookup } = await import('node:dns/promises');
const results = await lookup(hostname, { all: true, verbatim: true });
return results.map((entry) => entry.address);
}
function detectIpVersion(candidate: string): 0 | 4 | 6 {
try {
parseIpv4ToInt(candidate);
return 4;
} catch {
// continue to ipv6 parsing
}
try {
parseIpv6ToBigInt(candidate);
return 6;
} catch {
return 0;
}
}
export async function assertExternalAddress(hostname: string, options: ExternalAddressOptions = {}): Promise<void> {
const { allowDnsRebindMitigation = true } = options;
const normalized = normalizeHostname(hostname);
if (!normalized) {
throw new Error('Hostname is required for external address check');
}
const allowInternalAddress = isInternalAddressAllowedForHostname(normalized);
const ipLiteral = normalizeIpLiteral(normalized);
const ipVersion = detectIpVersion(ipLiteral);
if (ipVersion !== 0) {
if (isIpInternalOrReserved(ipLiteral)) {
throw new Error(`Blocked internal or reserved address: ${ipLiteral}`);
}
return;
}
const firstLookup = await lookupAll(normalized);
if (firstLookup.length === 0) {
throw new Error(`No DNS records found for hostname: ${normalized}`);
}
for (const address of firstLookup) {
if (isIpInternalOrReserved(address) && !allowInternalAddress) {
throw new Error(`Blocked internal or reserved address for hostname ${normalized}: ${address}`);
}
}
if (allowDnsRebindMitigation) {
const secondLookup = await lookupAll(normalized);
for (const address of secondLookup) {
if (isIpInternalOrReserved(address) && !allowInternalAddress) {
throw new Error(`Blocked internal or reserved address for hostname ${normalized}: ${address}`);
}
}
}
}

Xet Storage Details

Size:
7.69 kB
·
Xet hash:
04b01723710ae2778a30a5ba84484cb452ce781b724b5285f45654ecd7a011c6

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.