{"record":{"id":"0ec8504e70f6062f","repo":"garrytan/gstack","slug":"blocked-parsed-hostname-is-a-cloud-metadata-en","errorCode":null,"errorMessage":"Blocked: ${parsed.hostname} is a cloud metadata endpoint. Access is denied for security.","messagePattern":"Blocked: (.+?) is a cloud metadata endpoint\\. Access is denied for security\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"browse/src/url-validation.ts","lineNumber":281,"sourceCode":"    // is suspicious. path.resolve will normalize it; check the result against safe dirs.\n    validateReadPath(fsPath);\n\n    // Return the canonical file:// URL derived from the filesystem path + original\n    // query + hash. This guarantees page.goto() gets a well-formed URL regardless\n    // of input shape while preserving SPA route/query params.\n    return pathToFileURL(fsPath).href + parsed.search + parsed.hash;\n  }\n\n  if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n    throw new Error(\n      `Blocked: scheme \"${parsed.protocol}\" is not allowed. Only http:, https:, and file: URLs are permitted.`\n    );\n  }\n\n  const hostname = normalizeHostname(parsed.hostname.toLowerCase());\n\n  if (BLOCKED_METADATA_HOSTS.has(hostname) || isMetadataIp(hostname) || isBlockedIpv6(hostname)) {\n    throw new Error(\n      `Blocked: ${parsed.hostname} is a cloud metadata endpoint. Access is denied for security.`\n    );\n  }\n\n  // DNS rebinding protection: resolve hostname and check if it points to metadata IPs.\n  // Skip for loopback/private IPs — they can't be DNS-rebinded and the async DNS\n  // resolution adds latency that breaks concurrent E2E tests under load.\n  const isLoopback = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';\n  const isPrivateNet = /^(10\\.|172\\.(1[6-9]|2[0-9]|3[01])\\.|192\\.168\\.)/.test(hostname);\n  if (!isLoopback && !isPrivateNet && await resolvesToBlockedIp(hostname)) {\n    throw new Error(\n      `Blocked: ${parsed.hostname} resolves to a cloud metadata IP. Possible DNS rebinding attack.`\n    );\n  }\n\n  return url;\n}\n","sourceCodeStart":263,"sourceCodeEnd":299,"githubUrl":"https://github.com/garrytan/gstack/blob/94993f74012782fd94416dd44b8314f6363a13a4/browse/src/url-validation.ts#L263-L299","documentation":"SSRF guard at url-validation.ts:278-282. Blocks literal cloud instance-metadata targets: the set BLOCKED_METADATA_HOSTS (169.254.169.254, metadata.google.internal, metadata.azure.internal, plus IPv6/IPv4-mapped forms), plus isMetadataIp which catches hex/decimal/octal encodings (0xA9FEA9FE, 2852039166), plus isBlockedIpv6 for fc/fd/fe80:: ranges.","triggerScenarios":"Navigating to 169.254.169.254 (or any numeric encoding of it), metadata.google.internal, metadata.azure.internal, fe80::1, or ::ffff:a9fe:a9fe. Any of these is the IMDS endpoint a cloud SSRF attack targets.","commonSituations":"A prompt-injection or scraped page payload that embeds the metadata IP; a misconfigured base URL that accidentally points at 169.254.x.x; test fixtures copied from a cloud abuse writeup.","solutions":["Remove the metadata endpoint from the navigation target","If a test needs metadata-like responses, mock them on http://localhost:<port>","Audit where the URL originated — a metadata target in user/scraped input is an injection signal","Replace numeric-IP forms with a safe local stub"],"exampleFix":"// before\nawait goto('http://169.254.169.254/latest/meta-data/')\n// after — mock locally\nawait goto('http://localhost:9000/mock-meta-data')","handlingStrategy":"validation","validationCode":"import { BLOCKED_METADATA_HOSTS } from './url-validation'  // if exported\nfunction looksLikeMetadataHost(u: string): boolean {\n  try {\n    const h = new URL(u).hostname.toLowerCase().replace(/^\\[|\\]$/g,'')\n    return BLOCKED_METADATA_HOSTS.has(h) || /^169\\.254\\./.test(h)\n  } catch { return false }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never let user/scraped input choose the navigation target without an allowlist","Mock metadata responses on http://localhost for tests","Treat any 169.254.x.x or metadata.* host in input as an injection signal"],"tags":["ssrf","cloud-metadata","security","url-validation","navigation"],"backgroundTag":null,"analyzedSha":"94993f74012782fd94416dd44b8314f6363a13a4","analyzedAt":"2026-08-12T04:06:23.140Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}