← All posts
·8 min read·GuardLayer

"url" parameter is not allowed: Next.js 16 next/image

Next.jsImagesSSRFSupabaseSecurity

"url" parameter is not allowed is the 400 /_next/image returns when it refuses an image src; since Next.js 16 a common cause is a host that resolves to a private IP, like the 127.0.0.1:54321 that supabase start serves Storage on. Gate images.dangerouslyAllowLocalIP to next dev and never commit it as true.

You upgrade to Next.js 16, run next dev, and every image from your local Supabase stack is a broken icon. /_next/image answers 400, and the terminal prints one of these:

# Next.js 16.0.x to 16.2.x
⨯ upstream image http://127.0.0.1:54321/storage/v1/object/public/avatars/u1.png resolved to private ip ["127.0.0.1"]

# Next.js 16.3.0 and later
⨯ upstream image http://127.0.0.1:54321/storage/v1/object/public/avatars/u1.png hostname resolved to private IP ["127.0.0.1"] If this is expected and you understand SSRF risk, use images.dangerouslyAllowLocalIP = true to continue.

Both lines come from fetchExternalImage in Next's image-optimizer.ts. The newer one, unchanged in 16.4.0, tells you to set the flag: right for next dev, wrong to commit.

What does "url" parameter is not allowed mean in next/image?

It means the image optimizer refused to fetch the src it was given. Three checks return that exact body:

  1. A relative src matched no localPatterns entry. With images.localPatterns unset, Next 16 defaults it to { pathname: '**', search: '' }, so a local src with a query string (/api/avatar?id=1) is refused until a pattern allows that search.
  2. An absolute src matched no remotePatterns entry. Protocol, hostname, port and pathname must all match; the un-configured host page covers each part.
  3. The host resolved to a private IP. New in 16, and the source of the log lines above.

In next dev, the default loader catches causes 1 and 2 while rendering <Image> and throws a more specific error. Cause 3 is only checked at fetch time, so a page that renders but gets a 400 plus that log line is cause 3.

Why does Next.js 16 block "upstream image resolved to private ip"?

Because 16.0 added an SSRF guard. Before fetching a remote image, fetchExternalImage resolves the hostname and refuses if any address is loopback, private, link-local or otherwise non-public: 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16 (home of cloud metadata endpoints) and their IPv6 counterparts. Next 15 had no such check: in v15.5.27, fetchExternalImage just calls fetch. The v16 upgrade guide lists it as a breaking change, next to the middleware-to-proxy rename.

Local Supabase hits it first. supabase start reports http://127.0.0.1:54321 as the project URL, and public objects (readable by anyone with the URL) are served under /storage/v1/object/public/, so every local image URL points at loopback. A localhost URL fails the same way, usually logging ["::1","127.0.0.1"].

Allow-listing that host doesn't help. The wynteam project's first fix (PR #564) did exactly that, and QA found local images still broken: the private-IP guard runs regardless of remotePatterns. So people reach for the flag:

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: "http",
        hostname: "127.0.0.1",
        port: "54321",
        pathname: "/storage/v1/object/public/**",
      },
      {
        protocol: "https",
        hostname: "<ref>.supabase.co",
        pathname: "/storage/v1/object/public/**",
      },
    ],
    // After the Next 16 upgrade every local Supabase Storage image
    // returned 400 "url" parameter is not allowed. This brings them back.
    dangerouslyAllowLocalIP: true,
  },
};

export default nextConfig;
guardlayer scan · next.config.tsLive engine output
Passed with warnings
92/100 · A
  • Warningnext.config.ts:20

    Image optimizer allowed to fetch private IPs

    Remove dangerouslyAllowLocalIP (it defaults to false). If you self-host and genuinely need internal images, keep remotePatterns pinned to the exact internal hosts and paths — never combine this with a '**' hostname.

Why is dangerouslyAllowLocalIP: true dangerous?

Because the whole private-IP check sits inside if (!dangerouslyAllowLocalIP). With the flag on, no address is checked on any hop: redirects re-enter fetchExternalImage with the same flag, and the Image docs say an allowed host's redirects are followed without validating remotePatterns again (up to maximumRedirects, 3 by default since v16).

/_next/image takes a url from anyone. A committed true puts you back where every 15.x app was: safe only while no allow-listed host redirects inward or resolves to an internal address, including hosts whose DNS you don't control. The fetch is mostly blind (non-images are rejected), but an upstream non-2xx status comes back as the response status, enough to map internal ports and paths.

Where it applies depends on the host:

  • Self-hosted Node (next start, standalone output, Docker on Railway, Render, Fly, a VPS or Coolify): Next's own optimizer serves /_next/image, so the flag really does switch the guard off.
  • Vercel: next/image uses Vercel's own pipeline, and the Build Output API's images config has no dangerouslyAllowLocalIP field, so the flag is likely ignored. We haven't verified how Vercel treats private IPs.
  • OpenNext on Cloudflare: its docs say the flag isn't supported and local IPs are always allowed when the URL matches remotePatterns.

The September 30, 2026 Next.js security release fixed CVE-2026-94483 (GHSA-cjq9-62q9-8jv4, High): SSRF in Image Optimization via an attacker-controlled, allow-listed remote URL. Apps with no images.remotePatterns are not affected. The patched 16.x release is 16.3.8 (GitHub's Advisory Database lists 16.3.8; the advisory on the Next.js repo still reads 16.3.?), and that tag also pins DNS resolution for external images (commit e002ad68), so the socket connects to the addresses that passed the check instead of resolving the name a second time.

Upgrade to 16.3.8 or later (16.4.0 is current), as with any next advisory. The pinned lookup sits inside the same if, so a committed true skips it too.

How do I fix "url" parameter is not allowed for localhost images?

Allow local IPs under next dev only. Keep both patterns and gate the flag:

const nextConfig: NextConfig = {
  images: {
    remotePatterns: [/* the same two patterns as above */],
    // Only `next dev` may fetch local Supabase Storage.
    dangerouslyAllowLocalIP: process.env.NODE_ENV === "development",
  },
};

next dev defaults NODE_ENV to development; next build and next start default it to production and warn if it's set to development. The v16 upgrade guide suggests the same check for dev-only logic in next.config. UNA's PR #5822 uses an opt-in env var that is "off by default, so deployed servers keep Next's SSRF protection", and wynteam's PR #565 gates on NODE_ENV !== "production". To key it to the dev server instead, export a config function and compare phase with PHASE_DEVELOPMENT_SERVER from next/constants.

Keep port: "54321" pinned, since a pattern without port matches every port on the machine, and allow-list localhost rather than 127.0.0.1 if that's what your client uses. On self-hosted Node the loopback pattern is inert in production: with the flag off, the private-IP check refuses it. OpenNext on Cloudflare skips that check, so there, keep the pattern out of production builds.

Or skip the flag: pass unoptimized={process.env.NODE_ENV === "development"} to those <Image>s and the browser loads the URL directly.

Other workarounds, and what they cost

  • Global images.unoptimized: true: safe but costly. The server fetches nothing, and every image ships at its original size and format.
  • A committed dangerouslyAllowLocalIP: true: fine on a laptop, a disabled SSRF guard on a self-hosted server.
  • A rewrites() proxy: the one that can widen your attack surface. A reply in a Korean Q&A thread about this log (on 16.0.8) maps /api/backend/:path* to http://localhost:8080/:path* and uses a relative src, which goes through fetchInternalImage and skips the private-IP check. If that service was meant to stay internal, production now serves all of it. No GuardLayer rule covers this; review rewrites() by hand with the other app-layer checks.

What GuardLayer flags, and what it doesn't

nextjs/image-allow-local-ip flags a literal, uncommented dangerouslyAllowLocalIP: true in next.config.{js,cjs,mjs,ts}, unless the file also sets a literal unoptimized: true or loader: 'custom' (then the optimizer fetches nothing).

It only flags the literal: the NODE_ENV gate passes, and so does any env-driven value, even one that is true in production. It can't see your deploy target, so it flags Vercel projects too; a move to Docker makes the flag live. Separately, general/vulnerable-dependency flags next 16.x declared below 16.3.8 in package.json (not your lockfile).

Quick self-check

# 1. Is the flag on, and is it a literal or env-driven?
grep -n "dangerouslyAllowLocalIP" next.config.*

# 2. Allow-listed hosts: http, loopback, private ranges, missing port?
grep -n -A6 "remotePatterns" next.config.*

# 3. Rewrites pointing at internal services
grep -nE "destination:\s*['\"]https?://(localhost|127\.|10\.|192\.168\.)" next.config.*

If production allow-lists a local host, ask your self-hosted deployment to fetch it:

curl -s -w "\n%{http_code}\n" \
  "https://your-app.example/_next/image?url=http%3A%2F%2F127.0.0.1%3A54321%2Fstorage%2Fv1%2Fobject%2Fpublic%2Fx.png&w=64&q=75"

A 400 with "url" parameter is not allowed means nothing was fetched: the guard refused it, or production doesn't allow-list that exact URL. A 500 (what wynteam's dev server returned with the flag on and nothing listening), a 400 saying the resource isn't a valid image, or an actual image means the server tried its own loopback interface.

FAQ

Why do my local Supabase images return 400 after upgrading to Next.js 16? supabase start serves Storage from 127.0.0.1:54321, and the Next.js 16 optimizer refuses private IPs; Next 15 didn't check. Enable dangerouslyAllowLocalIP under next dev only, or use unoptimized on those images in development.

What does "hostname resolved to private IP" mean in Next.js? It's the 16.3+ wording of the same refusal: the image host resolved to a loopback, private, link-local or other non-public address, so the optimizer returned 400. Earlier 16.x releases print resolved to private ip.

Is dangerouslyAllowLocalIP safe in production? Only on a self-hosted app that really serves images from a private network, with every allow-listed host yours and pinned by protocol, hostname, port and path. A dev workaround doesn't qualify.

Does dangerouslyAllowLocalIP do anything on Vercel? Probably not: Vercel's own pipeline serves next/image, and its Build Output API images config has no such field. On self-hosted Node it turns the private-IP check off.

Will upgrading to Next.js 16.3.8 or later fix the error? No. The 400 is the optimizer working as designed. 16.3.8 added DNS pinning to that check, but a flag set to true skips it.

Catch this before it ships — free

Scan your files for this and 35 other Next.js + Supabase issues, with the exact fix inline. Connect GitHub and GuardLayer checks every push too — free on public repos.

No signup, no card. Raw files aren’t kept; only the report is saved.

Keep reading

A Solvion project — see also Reglog — EU AI Act changelog, Proceedly, Solenna and Solvion Solutions.