UNPKG

@langchain/core

Version:
1 lines 14.8 kB
{"version":3,"file":"ssrf.cjs","names":[],"sources":["../../src/utils/ssrf.ts"],"sourcesContent":["// Private IP ranges (RFC 1918, loopback, link-local, etc.)\nconst PRIVATE_IP_RANGES = [\n \"10.0.0.0/8\",\n \"172.16.0.0/12\",\n \"192.168.0.0/16\",\n \"127.0.0.0/8\",\n \"169.254.0.0/16\",\n \"0.0.0.0/8\",\n \"::1/128\",\n \"fc00::/7\",\n \"fe80::/10\",\n \"ff00::/8\",\n];\n\n// Cloud metadata IPs\nconst CLOUD_METADATA_IPS = [\n \"169.254.169.254\",\n \"169.254.170.2\",\n \"100.100.100.200\",\n];\n\n// Cloud metadata hostnames (case-insensitive)\nconst CLOUD_METADATA_HOSTNAMES = [\n \"metadata.google.internal\",\n \"metadata\",\n \"instance-data\",\n];\n\n// Localhost variations\nconst LOCALHOST_NAMES = [\"localhost\", \"localhost.localdomain\"];\n\n/**\n * IPv4 regex: four octets 0-255\n */\nconst IPV4_REGEX =\n /^(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)$/;\n\n/**\n * Check if a string is a valid IPv4 address.\n */\nfunction isIPv4(ip: string): boolean {\n return IPV4_REGEX.test(ip);\n}\n\n/**\n * Check if a string is a valid IPv6 address.\n * Uses expandIpv6 for validation.\n */\nfunction isIPv6(ip: string): boolean {\n return expandIpv6(ip) !== null;\n}\n\n/**\n * Check if a string is a valid IP address (IPv4 or IPv6).\n */\nfunction isIP(ip: string): boolean {\n return isIPv4(ip) || isIPv6(ip);\n}\n\n/**\n * Parse an IP address string to an array of integers (for IPv4) or an array of 16-bit values (for IPv6)\n * Returns null if the IP is invalid.\n */\nfunction parseIp(ip: string): number[] | null {\n if (isIPv4(ip)) {\n return ip.split(\".\").map((octet) => parseInt(octet, 10));\n } else if (isIPv6(ip)) {\n // Normalize IPv6\n const expanded = expandIpv6(ip);\n if (!expanded) return null;\n const parts = expanded.split(\":\");\n const result: number[] = [];\n for (const part of parts) {\n result.push(parseInt(part, 16));\n }\n return result;\n }\n return null;\n}\n\n/**\n * Expand compressed IPv6 address to full form.\n */\nfunction expandIpv6(ip: string): string | null {\n // Basic structural validation\n if (!ip || typeof ip !== \"string\") return null;\n\n // Must contain at least one colon\n if (!ip.includes(\":\")) return null;\n\n // Check for invalid characters\n if (!/^[0-9a-fA-F:]+$/.test(ip)) return null;\n\n let normalized = ip;\n\n // Handle :: compression\n if (normalized.includes(\"::\")) {\n const parts = normalized.split(\"::\");\n if (parts.length > 2) return null; // Multiple :: is invalid\n\n const [left, right] = parts;\n const leftParts = left ? left.split(\":\") : [];\n const rightParts = right ? right.split(\":\") : [];\n const missing = 8 - (leftParts.length + rightParts.length);\n\n if (missing < 0) return null;\n\n const zeros = Array(missing).fill(\"0\");\n normalized = [...leftParts, ...zeros, ...rightParts]\n .filter((p) => p !== \"\")\n .join(\":\");\n }\n\n const parts = normalized.split(\":\");\n if (parts.length !== 8) return null;\n\n // Validate each part is a valid hex group (1-4 chars)\n for (const part of parts) {\n if (part.length === 0 || part.length > 4) return null;\n if (!/^[0-9a-fA-F]+$/.test(part)) return null;\n }\n\n return parts.map((p) => p.padStart(4, \"0\").toLowerCase()).join(\":\");\n}\n\n/**\n * Parse CIDR notation (e.g., \"192.168.0.0/24\") into network address and prefix length.\n */\nfunction parseCidr(\n cidr: string\n): { addr: number[]; prefixLen: number; isIpv6: boolean } | null {\n const [addrStr, prefixStr] = cidr.split(\"/\");\n if (!addrStr || !prefixStr) {\n return null;\n }\n\n const addr = parseIp(addrStr);\n if (!addr) {\n return null;\n }\n\n const prefixLen = parseInt(prefixStr, 10);\n if (isNaN(prefixLen)) {\n return null;\n }\n\n const isIpv6 = isIPv6(addrStr);\n\n if (isIpv6 && prefixLen > 128) {\n return null;\n }\n if (!isIpv6 && prefixLen > 32) {\n return null;\n }\n\n return { addr, prefixLen, isIpv6 };\n}\n\n/**\n * Check if an IP address is in a given CIDR range.\n */\nfunction isIpInCidr(ip: string, cidr: string): boolean {\n const ipParsed = parseIp(ip);\n if (!ipParsed) {\n return false;\n }\n\n const cidrParsed = parseCidr(cidr);\n if (!cidrParsed) {\n return false;\n }\n\n // Check IPv4 vs IPv6 mismatch\n const isIpv6 = isIPv6(ip);\n if (isIpv6 !== cidrParsed.isIpv6) {\n return false;\n }\n\n const { addr: cidrAddr, prefixLen } = cidrParsed;\n\n // Convert to bits and compare\n if (isIpv6) {\n // IPv6: each element is 16 bits\n for (let i = 0; i < Math.ceil(prefixLen / 16); i++) {\n const bitsToCheck = Math.min(16, prefixLen - i * 16);\n const mask = (0xffff << (16 - bitsToCheck)) & 0xffff;\n if ((ipParsed[i] & mask) !== (cidrAddr[i] & mask)) {\n return false;\n }\n }\n } else {\n // IPv4: each element is 8 bits\n for (let i = 0; i < Math.ceil(prefixLen / 8); i++) {\n const bitsToCheck = Math.min(8, prefixLen - i * 8);\n const mask = (0xff << (8 - bitsToCheck)) & 0xff;\n if ((ipParsed[i] & mask) !== (cidrAddr[i] & mask)) {\n return false;\n }\n }\n }\n\n return true;\n}\n\n/**\n * Check if an IP address is private (RFC 1918, loopback, link-local, etc.)\n */\nexport function isPrivateIp(ip: string): boolean {\n // Validate it's a proper IP\n if (!isIP(ip)) {\n return false;\n }\n\n for (const range of PRIVATE_IP_RANGES) {\n if (isIpInCidr(ip, range)) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Check if a hostname or IP is a known cloud metadata endpoint.\n */\nexport function isCloudMetadata(hostname: string, ip?: string): boolean {\n // Check if it's a known metadata IP\n if (CLOUD_METADATA_IPS.includes(ip || \"\")) {\n return true;\n }\n\n // Check if hostname matches (case-insensitive)\n const lowerHostname = hostname.toLowerCase();\n if (CLOUD_METADATA_HOSTNAMES.includes(lowerHostname)) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Check if a hostname or IP is localhost.\n */\nexport function isLocalhost(hostname: string, ip?: string): boolean {\n // Check if it's a localhost IP\n if (ip) {\n // Check for typical localhost IPs (loopback range)\n if (ip === \"127.0.0.1\" || ip === \"::1\" || ip === \"0.0.0.0\") {\n return true;\n }\n // Check if IP starts with 127. (entire loopback range)\n if (ip.startsWith(\"127.\")) {\n return true;\n }\n }\n\n // Check if hostname is localhost\n const lowerHostname = hostname.toLowerCase();\n if (LOCALHOST_NAMES.includes(lowerHostname)) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Validate that a URL is safe to connect to.\n * Performs static validation checks against hostnames and direct IP addresses.\n * Does not perform DNS resolution.\n *\n * @param url URL to validate\n * @param options.allowPrivate Allow private IPs (default: false)\n * @param options.allowHttp Allow http:// scheme (default: false)\n * @returns The validated URL\n * @throws Error if URL is not safe\n */\nexport function validateSafeUrl(\n url: string,\n options?: { allowPrivate?: boolean; allowHttp?: boolean }\n): string {\n const allowPrivate = options?.allowPrivate ?? false;\n const allowHttp = options?.allowHttp ?? false;\n\n try {\n let parsedUrl: URL;\n try {\n parsedUrl = new URL(url);\n } catch {\n throw new Error(`Invalid URL: ${url}`);\n }\n\n const hostname = parsedUrl.hostname;\n if (!hostname) {\n throw new Error(\"URL missing hostname.\");\n }\n\n // Check if it's a cloud metadata endpoint (always blocked)\n if (isCloudMetadata(hostname)) {\n throw new Error(`URL points to cloud metadata endpoint: ${hostname}`);\n }\n\n // Check if it's localhost (blocked unless allowPrivate is true)\n if (isLocalhost(hostname)) {\n if (!allowPrivate) {\n throw new Error(`URL points to localhost: ${hostname}`);\n }\n return url;\n }\n\n // Check scheme (after localhost checks to give better error messages)\n const scheme = parsedUrl.protocol;\n if (scheme !== \"http:\" && scheme !== \"https:\") {\n throw new Error(\n `Invalid URL scheme: ${scheme}. Only http and https are allowed.`\n );\n }\n\n if (scheme === \"http:\" && !allowHttp) {\n throw new Error(\n \"HTTP scheme not allowed. Use HTTPS or set allowHttp: true.\"\n );\n }\n\n // If hostname is already an IP, validate it directly\n if (isIP(hostname)) {\n const ip = hostname;\n\n // Check if it's localhost first (before private IP check)\n if (isLocalhost(hostname, ip)) {\n if (!allowPrivate) {\n throw new Error(`URL points to localhost: ${hostname}`);\n }\n return url;\n }\n\n // Cloud metadata is always blocked\n if (isCloudMetadata(hostname, ip)) {\n throw new Error(\n `URL resolves to cloud metadata IP: ${ip} (${hostname})`\n );\n }\n\n // Check private IPs\n if (isPrivateIp(ip)) {\n if (!allowPrivate) {\n throw new Error(\n `URL resolves to private IP: ${ip} (${hostname}). Set allowPrivate: true to allow.`\n );\n }\n }\n\n return url;\n }\n\n // For regular hostnames, we've already done all hostname-based checks above\n // (cloud metadata, localhost). If those passed, the URL is safe.\n // We don't perform DNS resolution in this environment-agnostic function.\n return url;\n } catch (error) {\n if (error && typeof error === \"object\" && \"message\" in error) {\n throw error;\n }\n throw new Error(`URL validation failed: ${error}`);\n }\n}\n\n/**\n * Check if a URL is safe to connect to (non-throwing version).\n *\n * @param url URL to check\n * @param options.allowPrivate Allow private IPs (default: false)\n * @param options.allowHttp Allow http:// scheme (default: false)\n * @returns true if URL is safe, false otherwise\n */\nexport function isSafeUrl(\n url: string,\n options?: { allowPrivate?: boolean; allowHttp?: boolean }\n): boolean {\n try {\n validateSafeUrl(url, options);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Check if two URLs have the same origin (scheme, host, port).\n * Uses semantic URL parsing to prevent SSRF bypasses via URL variations.\n *\n * @param url1 First URL\n * @param url2 Second URL\n * @returns true if both URLs have the same origin, false otherwise\n */\nexport function isSameOrigin(url1: string, url2: string): boolean {\n try {\n return new URL(url1).origin === new URL(url2).origin;\n } catch {\n return false;\n }\n}\n"],"mappings":";;;;;;;;;;;;AACA,MAAM,oBAAoB;CACxB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAGD,MAAM,qBAAqB;CACzB;CACA;CACA;CACD;AAGD,MAAM,2BAA2B;CAC/B;CACA;CACA;CACD;AAGD,MAAM,kBAAkB,CAAC,aAAa,wBAAwB;;;;AAK9D,MAAM,aACJ;;;;AAKF,SAAS,OAAO,IAAqB;AACnC,QAAO,WAAW,KAAK,GAAG;;;;;;AAO5B,SAAS,OAAO,IAAqB;AACnC,QAAO,WAAW,GAAG,KAAK;;;;;AAM5B,SAAS,KAAK,IAAqB;AACjC,QAAO,OAAO,GAAG,IAAI,OAAO,GAAG;;;;;;AAOjC,SAAS,QAAQ,IAA6B;AAC5C,KAAI,OAAO,GAAG,CACZ,QAAO,GAAG,MAAM,IAAI,CAAC,KAAK,UAAU,SAAS,OAAO,GAAG,CAAC;UAC/C,OAAO,GAAG,EAAE;EAErB,MAAM,WAAW,WAAW,GAAG;AAC/B,MAAI,CAAC,SAAU,QAAO;EACtB,MAAM,QAAQ,SAAS,MAAM,IAAI;EACjC,MAAM,SAAmB,EAAE;AAC3B,OAAK,MAAM,QAAQ,MACjB,QAAO,KAAK,SAAS,MAAM,GAAG,CAAC;AAEjC,SAAO;;AAET,QAAO;;;;;AAMT,SAAS,WAAW,IAA2B;AAE7C,KAAI,CAAC,MAAM,OAAO,OAAO,SAAU,QAAO;AAG1C,KAAI,CAAC,GAAG,SAAS,IAAI,CAAE,QAAO;AAG9B,KAAI,CAAC,kBAAkB,KAAK,GAAG,CAAE,QAAO;CAExC,IAAI,aAAa;AAGjB,KAAI,WAAW,SAAS,KAAK,EAAE;EAC7B,MAAM,QAAQ,WAAW,MAAM,KAAK;AACpC,MAAI,MAAM,SAAS,EAAG,QAAO;EAE7B,MAAM,CAAC,MAAM,SAAS;EACtB,MAAM,YAAY,OAAO,KAAK,MAAM,IAAI,GAAG,EAAE;EAC7C,MAAM,aAAa,QAAQ,MAAM,MAAM,IAAI,GAAG,EAAE;EAChD,MAAM,UAAU,KAAK,UAAU,SAAS,WAAW;AAEnD,MAAI,UAAU,EAAG,QAAO;EAExB,MAAM,QAAQ,MAAM,QAAQ,CAAC,KAAK,IAAI;AACtC,eAAa;GAAC,GAAG;GAAW,GAAG;GAAO,GAAG;GAAW,CACjD,QAAQ,MAAM,MAAM,GAAG,CACvB,KAAK,IAAI;;CAGd,MAAM,QAAQ,WAAW,MAAM,IAAI;AACnC,KAAI,MAAM,WAAW,EAAG,QAAO;AAG/B,MAAK,MAAM,QAAQ,OAAO;AACxB,MAAI,KAAK,WAAW,KAAK,KAAK,SAAS,EAAG,QAAO;AACjD,MAAI,CAAC,iBAAiB,KAAK,KAAK,CAAE,QAAO;;AAG3C,QAAO,MAAM,KAAK,MAAM,EAAE,SAAS,GAAG,IAAI,CAAC,aAAa,CAAC,CAAC,KAAK,IAAI;;;;;AAMrE,SAAS,UACP,MAC+D;CAC/D,MAAM,CAAC,SAAS,aAAa,KAAK,MAAM,IAAI;AAC5C,KAAI,CAAC,WAAW,CAAC,UACf,QAAO;CAGT,MAAM,OAAO,QAAQ,QAAQ;AAC7B,KAAI,CAAC,KACH,QAAO;CAGT,MAAM,YAAY,SAAS,WAAW,GAAG;AACzC,KAAI,MAAM,UAAU,CAClB,QAAO;CAGT,MAAM,SAAS,OAAO,QAAQ;AAE9B,KAAI,UAAU,YAAY,IACxB,QAAO;AAET,KAAI,CAAC,UAAU,YAAY,GACzB,QAAO;AAGT,QAAO;EAAE;EAAM;EAAW;EAAQ;;;;;AAMpC,SAAS,WAAW,IAAY,MAAuB;CACrD,MAAM,WAAW,QAAQ,GAAG;AAC5B,KAAI,CAAC,SACH,QAAO;CAGT,MAAM,aAAa,UAAU,KAAK;AAClC,KAAI,CAAC,WACH,QAAO;CAIT,MAAM,SAAS,OAAO,GAAG;AACzB,KAAI,WAAW,WAAW,OACxB,QAAO;CAGT,MAAM,EAAE,MAAM,UAAU,cAAc;AAGtC,KAAI,OAEF,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,KAAK,YAAY,GAAG,EAAE,KAAK;EAElD,MAAM,OAAQ,SAAW,KADL,KAAK,IAAI,IAAI,YAAY,IAAI,GAAG,GACN;AAC9C,OAAK,SAAS,KAAK,WAAW,SAAS,KAAK,MAC1C,QAAO;;KAKX,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,KAAK,YAAY,EAAE,EAAE,KAAK;EAEjD,MAAM,OAAQ,OAAS,IADH,KAAK,IAAI,GAAG,YAAY,IAAI,EAAE,GACP;AAC3C,OAAK,SAAS,KAAK,WAAW,SAAS,KAAK,MAC1C,QAAO;;AAKb,QAAO;;;;;AAMT,SAAgB,YAAY,IAAqB;AAE/C,KAAI,CAAC,KAAK,GAAG,CACX,QAAO;AAGT,MAAK,MAAM,SAAS,kBAClB,KAAI,WAAW,IAAI,MAAM,CACvB,QAAO;AAIX,QAAO;;;;;AAMT,SAAgB,gBAAgB,UAAkB,IAAsB;AAEtE,KAAI,mBAAmB,SAAS,MAAM,GAAG,CACvC,QAAO;CAIT,MAAM,gBAAgB,SAAS,aAAa;AAC5C,KAAI,yBAAyB,SAAS,cAAc,CAClD,QAAO;AAGT,QAAO;;;;;AAMT,SAAgB,YAAY,UAAkB,IAAsB;AAElE,KAAI,IAAI;AAEN,MAAI,OAAO,eAAe,OAAO,SAAS,OAAO,UAC/C,QAAO;AAGT,MAAI,GAAG,WAAW,OAAO,CACvB,QAAO;;CAKX,MAAM,gBAAgB,SAAS,aAAa;AAC5C,KAAI,gBAAgB,SAAS,cAAc,CACzC,QAAO;AAGT,QAAO;;;;;;;;;;;;;AAcT,SAAgB,gBACd,KACA,SACQ;CACR,MAAM,eAAe,SAAS,gBAAgB;CAC9C,MAAM,YAAY,SAAS,aAAa;AAExC,KAAI;EACF,IAAI;AACJ,MAAI;AACF,eAAY,IAAI,IAAI,IAAI;UAClB;AACN,SAAM,IAAI,MAAM,gBAAgB,MAAM;;EAGxC,MAAM,WAAW,UAAU;AAC3B,MAAI,CAAC,SACH,OAAM,IAAI,MAAM,wBAAwB;AAI1C,MAAI,gBAAgB,SAAS,CAC3B,OAAM,IAAI,MAAM,0CAA0C,WAAW;AAIvE,MAAI,YAAY,SAAS,EAAE;AACzB,OAAI,CAAC,aACH,OAAM,IAAI,MAAM,4BAA4B,WAAW;AAEzD,UAAO;;EAIT,MAAM,SAAS,UAAU;AACzB,MAAI,WAAW,WAAW,WAAW,SACnC,OAAM,IAAI,MACR,uBAAuB,OAAO,oCAC/B;AAGH,MAAI,WAAW,WAAW,CAAC,UACzB,OAAM,IAAI,MACR,6DACD;AAIH,MAAI,KAAK,SAAS,EAAE;GAClB,MAAM,KAAK;AAGX,OAAI,YAAY,UAAU,GAAG,EAAE;AAC7B,QAAI,CAAC,aACH,OAAM,IAAI,MAAM,4BAA4B,WAAW;AAEzD,WAAO;;AAIT,OAAI,gBAAgB,UAAU,GAAG,CAC/B,OAAM,IAAI,MACR,sCAAsC,GAAG,IAAI,SAAS,GACvD;AAIH,OAAI,YAAY,GAAG,EACjB;QAAI,CAAC,aACH,OAAM,IAAI,MACR,+BAA+B,GAAG,IAAI,SAAS,qCAChD;;AAIL,UAAO;;AAMT,SAAO;UACA,OAAO;AACd,MAAI,SAAS,OAAO,UAAU,YAAY,aAAa,MACrD,OAAM;AAER,QAAM,IAAI,MAAM,0BAA0B,QAAQ;;;;;;;;;;;AAYtD,SAAgB,UACd,KACA,SACS;AACT,KAAI;AACF,kBAAgB,KAAK,QAAQ;AAC7B,SAAO;SACD;AACN,SAAO;;;;;;;;;;;AAYX,SAAgB,aAAa,MAAc,MAAuB;AAChE,KAAI;AACF,SAAO,IAAI,IAAI,KAAK,CAAC,WAAW,IAAI,IAAI,KAAK,CAAC;SACxC;AACN,SAAO"}