@xapp/arachne-utils
Version:
157 lines • 4.97 kB
JavaScript
;
/*! 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