Harden Client URL Fetches

Any client-controlled URL (including OpenAI image_url) must be treated as SSRF-/DoS-/parsing-risk input and validated *at every transition* before connecting.

copy reviewer prompt

Prompt

Reviewer Prompt

Any client-controlled URL (including OpenAI image_url) must be treated as SSRF-/DoS-/parsing-risk input and validated at every transition before connecting.

Apply this standard:

  • Strict outbound policy: allow only http(s); reject embedded creds, localhost, link-local/private/CGNAT, and metadata hostnames.
  • Redirect safety: use redirect: "manual"; for each hop, resolve + validate again and enforce a small maximum redirect count.
  • DNS rebinding safety: after hostname validation, resolve IPs and reject if any resolved address is private/local/link-local/metadata.
  • Resource caps: cap wall-clock fetch time and final decoded/received size (and ideally cheap pre-checks).
  • Content integrity: verify content-type (and for data: URLs: enforce image/*, base64-only as required).
  • Fail closed with sanitized errors: throw a 400-class error with a message that does not echo attacker-controlled URLs/hosts.
  • No “wildcard” targets for client-driven messaging: when sending postMessage, explicitly target the intended loopback origins (no *).

Example pattern (server-side fetch):

async function safeFetchImageBytes(inputUrl: string) {
  // 1) Parse + enforce strict outbound policy
  let current = inputUrl;
  for (let hop = 0; hop <= 3; hop++) {
    const parsed = parseAndValidatePublicUrl(current); // strict scheme/host policy

    // 2) DNS rebinding defense (reject if any IP answer is private)
    // validateResolvedIps(parsed.hostname)

    // 3) Manual redirect handling
    const res = await fetch(parsed.toString(), { method: 'GET', redirect: 'manual' });
    if (res.status >= 300 && res.status < 400 && res.headers.get('location')) {
      current = new URL(res.headers.get('location')!, parsed).toString();
      continue; // re-validate next hop
    }

    // 4) Verify content-type and read with a size cap
    const ct = (res.headers.get('content-type') || '').toLowerCase();
    if (!ct.startsWith('image/')) throw new Error('Image content-type required');
    const data = await readCapped(res, 1 * 1024 * 1024);
    return data;
  }
  throw new Error('Too many redirects');
}

Source discussions