UNPKG

@langchain/core

Version:
1 lines 15 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;AACF;AAGA,MAAM,qBAAqB;CACzB;CACA;CACA;AACF;AAGA,MAAM,2BAA2B;CAC/B;CACA;CACA;AACF;AAGA,MAAM,kBAAkB,CAAC,aAAa,uBAAuB;;;;AAK7D,MAAM,aACJ;;;;AAKF,SAAS,OAAO,IAAqB;CACnC,OAAO,WAAW,KAAK,EAAE;AAC3B;;;;;AAMA,SAAS,OAAO,IAAqB;CACnC,OAAO,WAAW,EAAE,MAAM;AAC5B;;;;AAKA,SAAS,KAAK,IAAqB;CACjC,OAAO,OAAO,EAAE,KAAK,OAAO,EAAE;AAChC;;;;;AAMA,SAAS,QAAQ,IAA6B;CAC5C,IAAI,OAAO,EAAE,GACX,OAAO,GAAG,MAAM,GAAG,CAAC,CAAC,KAAK,UAAU,SAAS,OAAO,EAAE,CAAC;MAClD,IAAI,OAAO,EAAE,GAAG;EAErB,MAAM,WAAW,WAAW,EAAE;EAC9B,IAAI,CAAC,UAAU,OAAO;EACtB,MAAM,QAAQ,SAAS,MAAM,GAAG;EAChC,MAAM,SAAmB,CAAC;EAC1B,KAAK,MAAM,QAAQ,OACjB,OAAO,KAAK,SAAS,MAAM,EAAE,CAAC;EAEhC,OAAO;CACT;CACA,OAAO;AACT;;;;AAKA,SAAS,WAAW,IAA2B;CAE7C,IAAI,CAAC,MAAM,OAAO,OAAO,UAAU,OAAO;CAG1C,IAAI,CAAC,GAAG,SAAS,GAAG,GAAG,OAAO;CAG9B,IAAI,CAAC,kBAAkB,KAAK,EAAE,GAAG,OAAO;CAExC,IAAI,aAAa;CAGjB,IAAI,WAAW,SAAS,IAAI,GAAG;EAC7B,MAAM,QAAQ,WAAW,MAAM,IAAI;EACnC,IAAI,MAAM,SAAS,GAAG,OAAO;EAE7B,MAAM,CAAC,MAAM,SAAS;EACtB,MAAM,YAAY,OAAO,KAAK,MAAM,GAAG,IAAI,CAAC;EAC5C,MAAM,aAAa,QAAQ,MAAM,MAAM,GAAG,IAAI,CAAC;EAC/C,MAAM,UAAU,KAAK,UAAU,SAAS,WAAW;EAEnD,IAAI,UAAU,GAAG,OAAO;EAExB,MAAM,QAAQ,MAAM,OAAO,CAAC,CAAC,KAAK,GAAG;EACrC,aAAa;GAAC,GAAG;GAAW,GAAG;GAAO,GAAG;EAAU,CAAC,CACjD,QAAQ,MAAM,MAAM,EAAE,CAAC,CACvB,KAAK,GAAG;CACb;CAEA,MAAM,QAAQ,WAAW,MAAM,GAAG;CAClC,IAAI,MAAM,WAAW,GAAG,OAAO;CAG/B,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,KAAK,WAAW,KAAK,KAAK,SAAS,GAAG,OAAO;EACjD,IAAI,CAAC,iBAAiB,KAAK,IAAI,GAAG,OAAO;CAC3C;CAEA,OAAO,MAAM,KAAK,MAAM,EAAE,SAAS,GAAG,GAAG,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,KAAK,GAAG;AACpE;;;;AAKA,SAAS,UACP,MAC+D;CAC/D,MAAM,CAAC,SAAS,aAAa,KAAK,MAAM,GAAG;CAC3C,IAAI,CAAC,WAAW,CAAC,WACf,OAAO;CAGT,MAAM,OAAO,QAAQ,OAAO;CAC5B,IAAI,CAAC,MACH,OAAO;CAGT,MAAM,YAAY,SAAS,WAAW,EAAE;CACxC,IAAI,MAAM,SAAS,GACjB,OAAO;CAGT,MAAM,SAAS,OAAO,OAAO;CAE7B,IAAI,UAAU,YAAY,KACxB,OAAO;CAET,IAAI,CAAC,UAAU,YAAY,IACzB,OAAO;CAGT,OAAO;EAAE;EAAM;EAAW;CAAO;AACnC;;;;AAKA,SAAS,WAAW,IAAY,MAAuB;CACrD,MAAM,WAAW,QAAQ,EAAE;CAC3B,IAAI,CAAC,UACH,OAAO;CAGT,MAAM,aAAa,UAAU,IAAI;CACjC,IAAI,CAAC,YACH,OAAO;CAIT,MAAM,SAAS,OAAO,EAAE;CACxB,IAAI,WAAW,WAAW,QACxB,OAAO;CAGT,MAAM,EAAE,MAAM,UAAU,cAAc;CAGtC,IAAI,QAEF,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,KAAK,YAAY,EAAE,GAAG,KAAK;EAElD,MAAM,OAAQ,SAAW,KADL,KAAK,IAAI,IAAI,YAAY,IAAI,EACT,IAAM;EAC9C,KAAK,SAAS,KAAK,WAAW,SAAS,KAAK,OAC1C,OAAO;CAEX;MAGA,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,KAAK,YAAY,CAAC,GAAG,KAAK;EAEjD,MAAM,OAAQ,OAAS,IADH,KAAK,IAAI,GAAG,YAAY,IAAI,CACX,IAAM;EAC3C,KAAK,SAAS,KAAK,WAAW,SAAS,KAAK,OAC1C,OAAO;CAEX;CAGF,OAAO;AACT;;;;AAKA,SAAgB,YAAY,IAAqB;CAE/C,IAAI,CAAC,KAAK,EAAE,GACV,OAAO;CAGT,KAAK,MAAM,SAAS,mBAClB,IAAI,WAAW,IAAI,KAAK,GACtB,OAAO;CAIX,OAAO;AACT;;;;AAKA,SAAgB,gBAAgB,UAAkB,IAAsB;CAEtE,IAAI,mBAAmB,SAAS,MAAM,EAAE,GACtC,OAAO;CAIT,MAAM,gBAAgB,SAAS,YAAY;CAC3C,IAAI,yBAAyB,SAAS,aAAa,GACjD,OAAO;CAGT,OAAO;AACT;;;;AAKA,SAAgB,YAAY,UAAkB,IAAsB;CAElE,IAAI,IAAI;EAEN,IAAI,OAAO,eAAe,OAAO,SAAS,OAAO,WAC/C,OAAO;EAGT,IAAI,GAAG,WAAW,MAAM,GACtB,OAAO;CAEX;CAGA,MAAM,gBAAgB,SAAS,YAAY;CAC3C,IAAI,gBAAgB,SAAS,aAAa,GACxC,OAAO;CAGT,OAAO;AACT;;;;;;;;;;;;AAaA,SAAgB,gBACd,KACA,SACQ;CACR,MAAM,eAAe,SAAS,gBAAgB;CAC9C,MAAM,YAAY,SAAS,aAAa;CAExC,IAAI;EACF,IAAI;EACJ,IAAI;GACF,YAAY,IAAI,IAAI,GAAG;EACzB,QAAQ;GACN,MAAM,IAAI,MAAM,gBAAgB,KAAK;EACvC;EAEA,MAAM,WAAW,UAAU;EAC3B,IAAI,CAAC,UACH,MAAM,IAAI,MAAM,uBAAuB;EAIzC,IAAI,gBAAgB,QAAQ,GAC1B,MAAM,IAAI,MAAM,0CAA0C,UAAU;EAItE,IAAI,YAAY,QAAQ,GAAG;GACzB,IAAI,CAAC,cACH,MAAM,IAAI,MAAM,4BAA4B,UAAU;GAExD,OAAO;EACT;EAGA,MAAM,SAAS,UAAU;EACzB,IAAI,WAAW,WAAW,WAAW,UACnC,MAAM,IAAI,MACR,uBAAuB,OAAO,mCAChC;EAGF,IAAI,WAAW,WAAW,CAAC,WACzB,MAAM,IAAI,MACR,4DACF;EAIF,IAAI,KAAK,QAAQ,GAAG;GAClB,MAAM,KAAK;GAGX,IAAI,YAAY,UAAU,EAAE,GAAG;IAC7B,IAAI,CAAC,cACH,MAAM,IAAI,MAAM,4BAA4B,UAAU;IAExD,OAAO;GACT;GAGA,IAAI,gBAAgB,UAAU,EAAE,GAC9B,MAAM,IAAI,MACR,sCAAsC,GAAG,IAAI,SAAS,EACxD;GAIF,IAAI,YAAY,EAAE,GACZ;QAAA,CAAC,cACH,MAAM,IAAI,MACR,+BAA+B,GAAG,IAAI,SAAS,oCACjD;GAAA;GAIJ,OAAO;EACT;EAKA,OAAO;CACT,SAAS,OAAO;EACd,IAAI,SAAS,OAAO,UAAU,YAAY,aAAa,OACrD,MAAM;EAER,MAAM,IAAI,MAAM,0BAA0B,OAAO;CACnD;AACF;;;;;;;;;AAUA,SAAgB,UACd,KACA,SACS;CACT,IAAI;EACF,gBAAgB,KAAK,OAAO;EAC5B,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;AAUA,SAAgB,aAAa,MAAc,MAAuB;CAChE,IAAI;EACF,OAAO,IAAI,IAAI,IAAI,CAAC,CAAC,WAAW,IAAI,IAAI,IAAI,CAAC,CAAC;CAChD,QAAQ;EACN,OAAO;CACT;AACF"}