{"record":{"id":"99694baa3529a773","repo":"garrytan/gstack","slug":"blocked-scheme-parsed-protocol-is-not-allowe","errorCode":null,"errorMessage":"Blocked: scheme \"${parsed.protocol}\" is not allowed. Only http:, https:, and file: URLs are permitted.","messagePattern":"Blocked: scheme \"(.+?)\" is not allowed\\. Only http:, https:, and file: URLs are permitted\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"browse/src/url-validation.ts","lineNumber":273,"sourceCode":"    try {\n      fsPath = fileURLToPath(parsed);\n    } catch (e: any) {\n      throw new Error(`Invalid file URL: ${url} (${e.message})`);\n    }\n\n    // Reject path traversal after decoding — e.g. file:///tmp/safe%2F..%2Fetc/passwd\n    // Note: fileURLToPath doesn't collapse .., so a literal '..' in the decoded path\n    // 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)) {","sourceCodeStart":255,"sourceCodeEnd":291,"githubUrl":"https://github.com/garrytan/gstack/blob/94993f74012782fd94416dd44b8314f6363a13a4/browse/src/url-validation.ts#L255-L291","documentation":"Navigation-safety guard at url-validation.ts:271-274. After file: is handled and http:/https: are allowed, any remaining scheme is rejected. This is an SSRF/script-injection control blocking data:, javascript:, chrome:, blob:, ftp:, about: and every other protocol from reaching page.goto().","triggerScenarios":"Calling goto/validateNavigationUrl with a URL whose protocol is not http:, https:, or file: — e.g. data:text/html,<h1>hi, javascript:alert(1), chrome://settings, blob:..., about:blank, ftp://host/f.","commonSituations":"Trying to load an inline HTML data URI as a fixture; using about:blank as a starting page; scraped content containing a blob: or chrome: link; test code that defaults to an empty data URI.","solutions":["Serve the content from http://localhost:<port> instead","For inline HTML, use the load-html command (setContent) rather than goto","Write the data URI's payload to a temp file and navigate via file:///","Use a plain http(s) URL for any real page"],"exampleFix":"// before\nawait goto('data:text/html,<h1>hello</h1>')\n// after — use load-html for inline content\nawait handleWriteCommand('load-html', ['--from-file', 'payload.json'], session, bm)","handlingStrategy":"validation","validationCode":"const ALLOWED = new Set(['http:', 'https:', 'file:'])\nfunction isAllowedScheme(u: string): boolean {\n  try { return ALLOWED.has(new URL(u).protocol) } catch { return false }\n}","typeGuard":"function isNavigableUrl(u: string): u is string {\n  try {\n    const p = new URL(u)\n    return p.protocol === 'http:' || p.protocol === 'https:' || p.protocol === 'file:'\n  } catch { return false }\n}","tryCatchPattern":null,"preventionTips":["For inline HTML, use the load-html command instead of data: URIs","Maintain an allowlist of schemes at the call site","Serve fixture content from http://localhost"],"tags":["url-validation","ssrf","scheme-allowlist","security","navigation"],"backgroundTag":null,"analyzedSha":"94993f74012782fd94416dd44b8314f6363a13a4","analyzedAt":"2026-08-12T04:06:23.140Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}