UNPKG

@xapp/arachne-utils

Version:

157 lines 4.97 kB
"use strict"; /*! Copyright (c) 2025, XAPP AI */ Object.defineProperty(exports, "__esModule", { value: true }); exports.isPrivateIP = isPrivateIP; exports.isSafeURL = isSafeURL; /** * Private IPv4 ranges as defined by RFC 1918 and other reserved ranges */ const PRIVATE_IPV4_RANGES = [ { min: '10.0.0.0', max: '10.255.255.255' }, // Class A private { min: '172.16.0.0', max: '172.31.255.255' }, // Class B private { min: '192.168.0.0', max: '192.168.255.255' }, // Class C private { min: '127.0.0.0', max: '127.255.255.255' }, // Loopback { min: '169.254.0.0', max: '169.254.255.255' }, // Link-local (AWS metadata) { min: '0.0.0.0', max: '0.255.255.255' }, // Current network ]; /** * Private/reserved IPv6 ranges */ const PRIVATE_IPV6_PATTERNS = [ /^::1$/, // Loopback /^::$/, // Unspecified /^fe80:/, // Link-local /^fc00:/, // Unique local /^fd00:/, // Unique local ]; /** * Blocked hostnames that should never be accessible */ const BLOCKED_HOSTNAMES = [ 'localhost', 'metadata.google.internal', // GCP metadata ]; /** * Convert IPv4 address string to a number for comparison * Returns -1 if the IP is invalid */ function ipv4ToNumber(ip) { const parts = ip.split('.').map(Number); // Validate that we have exactly 4 parts and each is in valid range (0-255) if (parts.length !== 4 || parts.some(p => p < 0 || p > 255 || isNaN(p))) { return -1; } return (parts[0] << 24) + (parts[1] << 16) + (parts[2] << 8) + parts[3]; } /** * Check if an IPv4 address is in a private range */ function isPrivateIPv4(ip) { const ipNum = ipv4ToNumber(ip); // Invalid IP address - treat as unsafe if (ipNum === -1) { return true; } for (const range of PRIVATE_IPV4_RANGES) { const minNum = ipv4ToNumber(range.min); const maxNum = ipv4ToNumber(range.max); if (ipNum >= minNum && ipNum <= maxNum) { return true; } } return false; } /** * Check if an IPv6 address is private/reserved */ function isPrivateIPv6(ip) { const normalized = ip.toLowerCase(); for (const pattern of PRIVATE_IPV6_PATTERNS) { if (pattern.test(normalized)) { return true; } } return false; } /** * Check if an IP address (v4 or v6) is private or reserved * * This includes: * - Loopback addresses (127.0.0.1, ::1) * - Private network ranges (10.x.x.x, 192.168.x.x, 172.16-31.x.x) * - Link-local addresses (169.254.x.x, fe80::) * - Other reserved ranges * * @param ip - The IP address to check * @returns true if the IP is private/reserved, false otherwise */ function isPrivateIP(ip) { // Check for IPv4 if (/^(\d{1,3}\.){3}\d{1,3}$/.test(ip)) { return isPrivateIPv4(ip); } // Check for IPv6 if (ip.includes(':')) { return isPrivateIPv6(ip); } return false; } /** * Check if a URL is safe to fetch (not vulnerable to SSRF attacks) * * This function blocks: * - localhost and 127.0.0.1 * - Private IP ranges (10.x.x.x, 192.168.x.x, 172.16-31.x.x) * - AWS metadata endpoint (169.254.169.254) * - IPv6 local addresses (::1, fe80::, etc.) * - Cloud provider metadata endpoints * * **Security Limitation**: This function only checks the URL at the time of the call. * It does not protect against DNS rebinding attacks, where a malicious domain could * resolve to a public IP during this check but then resolve to a private IP (e.g., 127.0.0.1) * during the actual fetch. For high-security environments, consider implementing additional * DNS resolution validation or using a network-level SSRF protection solution. * * @param url - The URL to validate * @returns true if the URL is safe to fetch, false if it should be blocked * * @example * ```typescript * if (!isSafeURL('http://localhost/file.pdf')) { * throw new Error('SSRF protection: Cannot fetch from localhost'); * } * ``` */ function isSafeURL(url) { let parsedUrl; try { parsedUrl = typeof url === 'string' ? new URL(url) : url; } catch (e) { // Invalid URL return false; } // Only allow HTTP and HTTPS protocols if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') { return false; } const hostname = parsedUrl.hostname.toLowerCase(); // Check blocked hostnames if (BLOCKED_HOSTNAMES.includes(hostname)) { return false; } // Check if hostname is an IP address (v4 or v6) // For IPv4 if (/^(\d{1,3}\.){3}\d{1,3}$/.test(hostname)) { return !isPrivateIPv4(hostname); } // For IPv6 (can be wrapped in brackets in hostname) const ipv6Match = hostname.match(/^\[?([a-f0-9:]+)\]?$/i); if (ipv6Match) { return !isPrivateIPv6(ipv6Match[1]); } // Hostname is a domain name, which is generally safe // (DNS rebinding attacks are a separate concern) return true; } //# sourceMappingURL=ssrfProtection.js.map