{"record":{"id":"553f11740b45663f","repo":"garrytan/gstack","slug":"unsupported-file-url-host-parsed-host-use-fil","errorCode":null,"errorMessage":"Unsupported file URL host: ${parsed.host}. Use file:///<absolute-path> for local files.","messagePattern":"Unsupported file URL host: (.+?)\\. Use file:///<absolute-path> for local files\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"browse/src/url-validation.ts","lineNumber":246,"sourceCode":"export async function validateNavigationUrl(url: string): Promise<string> {\n  // Normalize non-standard file:// shapes before the URL parser sees them.\n  let normalized = url;\n  if (url.toLowerCase().startsWith('file:')) {\n    normalized = normalizeFileUrl(url);\n  }\n\n  let parsed: URL;\n  try {\n    parsed = new URL(normalized);\n  } catch {\n    throw new Error(`Invalid URL: ${url}`);\n  }\n\n  // file:// path: validate against safe-dirs and allow; otherwise defer to http(s) logic.\n  if (parsed.protocol === 'file:') {\n    // Reject non-empty non-localhost hosts (UNC / network paths).\n    if (parsed.host !== '' && parsed.host.toLowerCase() !== 'localhost') {\n      throw new Error(\n        `Unsupported file URL host: ${parsed.host}. Use file:///<absolute-path> for local files.`\n      );\n    }\n\n    // Convert URL → filesystem path with proper decoding (handles %20, %2F, etc.)\n    // fileURLToPath strips query + hash; we reattach them after validation so SPA\n    // fixture URLs like file:///tmp/app.html?route=home#login survive intact.\n    let fsPath: string;\n    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);","sourceCodeStart":228,"sourceCodeEnd":264,"githubUrl":"https://github.com/garrytan/gstack/blob/94993f74012782fd94416dd44b8314f6363a13a4/browse/src/url-validation.ts#L228-L264","documentation":"The navigation validator rejects file: URLs whose host component is anything other than empty or 'localhost'. The check at url-validation.ts:240-247 (parsed.host !== '' && parsed.host.toLowerCase() !== 'localhost') blocks UNC and network-share paths, which could otherwise let a sandboxed browser read from remote shares. Only local absolute paths via file:///<path> are permitted.","triggerScenarios":"Calling browse goto (or any path through validateNavigationUrl) with a file: URL that carries a host, e.g. file://server/share/x.html, file://nas/data.html, or file://home/user/foo.html (two slashes + name = host 'home'). On Windows, a pasted UNC path file://\\\\server\\share also parses to a non-empty host.","commonSituations":"Pasting a Windows UNC path verbatim; writing file:// instead of file:/// before an absolute path; templating URLs with a variable that injects a hostname; macOS/Linux users copying a 'file://host' link from a file manager.","solutions":["Use three slashes plus an absolute path: file:///home/user/foo.html or file:///C:/Users/me/foo.html.","For UNC/network shares, mount the share locally first (e.g. /mnt/share) and reference the mount point with file:///","If a literal host is intentional for a local server, use http://localhost:<port> instead of file://localhost","Build file URLs programmatically with pathToFileURL(absolutePath).href so the host is always empty"],"exampleFix":"// before\nawait goto('file://server/share/report.html')\n// after\nawait goto('file:///mnt/share/report.html')\n// or build canonically\nimport { pathToFileURL } from 'node:url'\nawait goto(pathToFileURL('/mnt/share/report.html').href)","handlingStrategy":"validation","validationCode":"import { pathToFileURL } from 'node:url'\n// Build file URLs canonically so host is always empty.\nfunction toFileUrl(absPath: string): string {\n  return pathToFileURL(absPath).href\n}\n// Reject any file: URL with a non-empty host before calling goto.\nfunction isLocalFileUrl(u: string): boolean {\n  let p: URL\n  try { p = new URL(u) } catch { return false }\n  if (p.protocol !== 'file:') return false\n  return p.host === '' || p.host.toLowerCase() === 'localhost'\n}","typeGuard":"function isSafeFileUrl(u: string): u is string {\n  try {\n    const p = new URL(u)\n    return p.protocol === 'file:' && (p.host === '' || p.host.toLowerCase() === 'localhost')\n  } catch { return false }\n}","tryCatchPattern":null,"preventionTips":["Always build file URLs with pathToFileURL(abs).href instead of string concatenation","Prefer file:/// (three slashes) plus an absolute path","Mount network shares locally before referencing them"],"tags":["url-validation","file-protocol","security","unc-path","navigation"],"backgroundTag":null,"analyzedSha":"94993f74012782fd94416dd44b8314f6363a13a4","analyzedAt":"2026-08-12T04:06:23.140Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}