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