vercel/next.js · error · ImageError

"url" parameter is not allowed

Error message

"url" parameter is not allowed

What it means

Thrown by fetchExternalImage (ImageError 400) when an external image URL's hostname resolves to a private/loopback IP and images.dangerouslyAllowLocalIP is false. Next.js performs DNS resolution (including all A/AAAA records) and rejects any private IP to prevent Server-Side Request Forgery (SSRF). This is a security guard, not a bug.

Source

Thrown at packages/next/src/server/image-optimizer.ts:882

    let ips = [hostname]
    if (!isIP(hostname)) {
      const records = await lookup(hostname, {
        family: 0,
        all: true,
        hints: ALL,
      }).catch((_) => [{ address: hostname }])
      ips = records.map((record) => record.address)
    }
    const privateIps = ips.filter((ip) => isPrivateIp(ip))
    if (privateIps.length > 0) {
      Log.error(
        'upstream image',
        href,
        'hostname resolved to private IP',
        JSON.stringify(privateIps),
        'If this is expected and you understand SSRF risk, use images.dangerouslyAllowLocalIP = true to continue.'
      )
      throw new ImageError(400, '"url" parameter is not allowed')
    }
  }
  const res = await fetch(href, {
    signal: AbortSignal.timeout(7_000),
    redirect: 'manual',
  }).catch((err) => err as Error)

  if (res instanceof Error) {
    const err = res as Error
    if (err.name === 'TimeoutError') {
      Log.error('upstream image response timed out for', href)
      throw new ImageError(
        504,
        '"url" parameter is valid but upstream response timed out'
      )
    }
    throw err
  }

View on GitHub (pinned to 0ae8c72462)

Solutions

  1. If SSRF risk is understood and acceptable, set images.dangerouslyAllowLocalIP = true in next.config.
  2. Whitelist only safe remote domains via images.remotePatterns/domains instead of arbitrary URLs.
  3. Move the upstream image host to a public IP/domain so it doesn't resolve privately.
  4. For internal images, serve them locally and use fetchInternalImage instead of an external URL.

Example fix

// before
module.exports = { images: { remotePatterns: [{ protocol: 'https', hostname: '**' }] } }

// after (explicit, allow local IP only if intended)
module.exports = {
  images: {
    dangerouslyAllowLocalIP: true,
    remotePatterns: [{ protocol: 'http', hostname: 'internal.local' }],
  },
}
Defensive patterns

Strategy: validation

Validate before calling

// Pre-validate remote image URLs against your allowed, non-private hosts
import { isIP } from 'net'
import { lookup } from 'dns/promises'
import ipaddr from 'ipaddr.js'
async function isAllowedRemoteImage(href: string, allowedHosts: string[]) {
  const { hostname } = new URL(href)
  if (!allowedHosts.includes(hostname)) return false
  const addrs = isIP(hostname) ? [hostname] : (await lookup(hostname, { all: true })).map(r => r.address)
  return addrs.every(a => !ipaddr.parse(a).range().match(/^(private|loopback|linkLocal)$/))
}

Prevention

When it happens

Trigger: An /_next/image?url=... request points at a hostname that resolves to a private IP (10.x, 192.168.x, 127.x, 169.254.x, etc.) and dangerouslyAllowLocalIP is not enabled. The privateIps filter is non-empty and the throw at line 882 fires.

Common situations: Pointing the image loader at an internal service on a private network. A hostname that is public today but resolves to a private range in a containerized/VPN environment. Local development with localhost-like hostnames. DNS rebinding where a hostname flips to a private IP.

Related errors


AI-assisted analysis of vercel/next.js@0ae8c72462 (2026-08-06). Data as JSON: /api/errors/55ebbf5b4b275519. Report an issue: GitHub.