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

  1. Remove the metadata endpoint from the navigation target
  2. If a test needs metadata-like responses, mock them on http://localhost:<port>
  3. Audit where the URL originated — a metadata target in user/scraped input is an injection signal
  4. 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

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


AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12). Data as JSON: /api/errors/0ec8504e70f6062f. Report an issue: GitHub.