garrytan/gstack · critical · Error
Blocked: ${parsed.hostname} is a cloud metadata endpoint. Ac
Error message
Blocked: ${parsed.hostname} is a cloud metadata endpoint. Access is denied for security. What it means
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.
Source
Thrown at browse/src/url-validation.ts:281
// is suspicious. path.resolve will normalize it; check the result against safe dirs.
validateReadPath(fsPath);
// Return the canonical file:// URL derived from the filesystem path + original
// query + hash. This guarantees page.goto() gets a well-formed URL regardless
// of input shape while preserving SPA route/query params.
return pathToFileURL(fsPath).href + parsed.search + parsed.hash;
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
throw new Error(
`Blocked: scheme "${parsed.protocol}" is not allowed. Only http:, https:, and file: URLs are permitted.`
);
}
const hostname = normalizeHostname(parsed.hostname.toLowerCase());
if (BLOCKED_METADATA_HOSTS.has(hostname) || isMetadataIp(hostname) || isBlockedIpv6(hostname)) {
throw new Error(
`Blocked: ${parsed.hostname} is a cloud metadata endpoint. Access is denied for security.`
);
}
// DNS rebinding protection: resolve hostname and check if it points to metadata IPs.
// Skip for loopback/private IPs — they can't be DNS-rebinded and the async DNS
// resolution adds latency that breaks concurrent E2E tests under load.
const isLoopback = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
const isPrivateNet = /^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)/.test(hostname);
if (!isLoopback && !isPrivateNet && await resolvesToBlockedIp(hostname)) {
throw new Error(
`Blocked: ${parsed.hostname} resolves to a cloud metadata IP. Possible DNS rebinding attack.`
);
}
return url;
}
View on GitHub (pinned to 94993f7401)
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
Example fix
// before
await goto('http://169.254.169.254/latest/meta-data/')
// after — mock locally
await goto('http://localhost:9000/mock-meta-data') Defensive patterns
Strategy: validation
Validate before calling
import { BLOCKED_METADATA_HOSTS } from './url-validation' // if exported
function looksLikeMetadataHost(u: string): boolean {
try {
const h = new URL(u).hostname.toLowerCase().replace(/^\[|\]$/g,'')
return BLOCKED_METADATA_HOSTS.has(h) || /^169\.254\./.test(h)
} catch { return false }
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Blocked: scheme "${parsed.protocol}" is not allowed. Only ht
- Blocked: ${parsed.hostname} resolves to a cloud metadata IP.
- Unsupported file URL host: ${parsed.host}. Use file:///<abso
- Invalid file URL: file:/// has no path. Use file:///<absolut
- Invalid file URL: ${url}. Use file:///<absolute-path> or fil
AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12).
Data as JSON: /api/errors/0ec8504e70f6062f.
Report an issue: GitHub.