{"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":"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"}