garrytan/gstack · critical · Error

Blocked: ${parsed.hostname} resolves to a cloud metadata IP.

Error message

Blocked: ${parsed.hostname} resolves to a cloud metadata IP. Possible DNS rebinding attack.

What it means

DNS-rebinding defense at url-validation.ts:287-293. After the literal-hostname block, the validator resolves the hostname (resolve4 + resolve6) and rejects it if any A/AAAA record points to a blocked metadata IP. Loopback and private-net literals are skipped to avoid latency in concurrent E2E runs.

Source

Thrown at browse/src/url-validation.ts:292

      `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. Do not navigate to untrusted or scraped hostnames without an allowlist
  2. Resolve the hostname yourself (dig <host>) and confirm it is not a metadata IP
  3. Route through a forward proxy that enforces destination allowlisting
  4. If the target is legitimate, point it at a safe IP and re-test
Defensive patterns

Strategy: validation

Validate before calling

import { resolve4, resolve6 } from 'node:dns/promises'
async function resolvesToMetadata(host: string): Promise<boolean> {
  const meta = new Set(['169.254.169.254'])
  try {
    const [v4, v6] = await Promise.all([resolve4(host), resolve6(host).catch(()=>[])])
    return [...v4, ...v6].some(a => meta.has(a) || /^fd|^fe[89ab]/.test(a))
  } catch { return false }
}

Prevention

When it happens

Trigger: A hostname whose DNS A or AAAA records resolve to 169.254.169.254 or another blocked IP — the classic rebinding payload where the name looks benign but the address is the metadata service.

Common situations: Attacker-controlled domain referenced in scraped/templated URLs; stale or poisoned /etc/hosts or corporate DNS; a wildcard DNS service (e.g. nip.io style) pointed at the metadata IP.

Related errors


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