DEV Community

Anas Sheikh
Anas Sheikh

Posted on

That Wildcard in Your next/image remotePatterns Config Might Be an SSRF Hole

If you've ever gotten tired of adding every single image domain to your Next.js config one by one, you've probably reached for a wildcard like this:

// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: '**', // "just allow everything, I'll deal with it later"
      },
    ],
  },
};
Enter fullscreen mode Exit fullscreen mode

It works. Every image URL you throw at next/image loads without complaint. It's also quietly turned your image optimizer into an open proxy that will fetch almost any URL an attacker gives it and return the response, which is a textbook Server-Side Request Forgery setup.

What next/image Is Actually Doing Under the Hood

When you use the next/image component with a remote URL, Next.js doesn't just display the image directly. Its image optimization endpoint fetches the image from the source URL on your server, processes it, and serves the optimized version to the browser. Your server is making an outbound HTTP request on behalf of whatever URL gets passed in.

<Image src={userSuppliedUrl} width={400} height={300} alt="Profile photo" />
Enter fullscreen mode Exit fullscreen mode

remotePatterns exists specifically to restrict which hostnames your server is allowed to fetch on someone's behalf. A tight allowlist means your server will only ever fetch images from domains you've explicitly approved. A wildcard hostname means your server will fetch from anywhere, including internal addresses it was never meant to reach from the outside.

Where This Actually Gets Exploited

If any part of your app lets a user influence the image URL passed to next/image, a profile picture URL, an avatar imported from a form field, an image pulled from user-submitted content, a wildcard remotePatterns config means that input can point your server's own outbound request at infrastructure it shouldn't be able to reach from the internet.

https://yourapp.com/_next/image?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/some-role&w=640&q=75
Enter fullscreen mode Exit fullscreen mode

That address is the cloud metadata endpoint many providers expose only to the instance itself, not to the public internet. If your server fetches it and returns something resembling image data back to the optimizer, you've potentially just used your own app to leak role credentials, internal service responses, or probe internal network addresses that were never meant to be reachable this way. The fact that the optimizer expects an image doesn't stop the fetch from happening against a non-image endpoint; it just means the response might fail to process as an image afterward, by which point the request has already gone out.

Why This Config Gets Written in the First Place

Nobody sets hostname: '**' out of carelessness exactly. It usually happens because a project pulls images from genuinely unpredictable sources, user-generated content, a CMS with dynamic asset domains, a multi-tenant app where each customer might host images somewhere different, and listing every possible domain up front feels impractical. The wildcard is the path of least resistance that makes the error message go away, and it keeps working correctly for every legitimate case, which is exactly why nobody revisits it.

The Fix: Scope the Wildcard as Tightly as the Use Case Allows

// next.config.js

// If you control the domains, list them explicitly, no wildcard at all
module.exports = {
  images: {
    remotePatterns: [
      { protocol: 'https', hostname: 'cdn.yourapp.com' },
      { protocol: 'https', hostname: 'images.yourcms.com' },
    ],
  },
};
Enter fullscreen mode Exit fullscreen mode

If you genuinely need to support unpredictable subdomains, a scoped wildcard is dramatically safer than an unscoped one:

// Only matches subdomains of a domain you actually control,
// not arbitrary external hosts
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: '*.yourcustomerssubdomains.com',
      },
    ],
  },
};
Enter fullscreen mode Exit fullscreen mode

And if the real requirement is "users submit arbitrary external image URLs," that's a case where remotePatterns alone was never the right control to rely on in the first place. Validate and resolve the URL's hostname server-side before it ever reaches the image component, explicitly reject private and link-local IP ranges, and consider proxying through a dedicated image-fetching service that's isolated from anything with network access to your internal infrastructure.

// A basic server-side guard before accepting a user-supplied image URL
import { isIP } from 'node:net';

const BLOCKED_HOSTS = ['localhost', '169.254.169.254'];

function isSafeImageUrl(url: string): boolean {
  try {
    const parsed = new URL(url);
    if (parsed.protocol !== 'https:') return false;
    if (BLOCKED_HOSTS.includes(parsed.hostname)) return false;
    // Also reject private IP ranges (10.x, 192.168.x, 172.16-31.x, 127.x) here
    return true;
  } catch {
    return false;
  }
}
Enter fullscreen mode Exit fullscreen mode

The Rule

Any config option that controls which outbound URLs your server is allowed to fetch deserves the same scrutiny you'd give a firewall rule, because that's functionally what it is. remotePatterns isn't just an image-loading convenience setting, it's an outbound allowlist, and a wildcard in it has the same shape as a security control that was turned off because it was inconvenient, not because it stopped mattering.

I keep this scoped tightly in the image-heavy dashboard templates on pixelanas.com, specifically because user-influenced image sources show up constantly in dashboard and SaaS products.

Get the templates: https://pixelanas.gumroad.com

Go check your own next.config.js right now for hostname: '**' or anything close to it. If you find it and any image URL is even partially user-influenced, this is worth fixing today, not on the next refactor pass. Drop what you find in the comments.


Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751

Top comments (1)

Collapse
 
dhruv_malaviya_cdcc71e595 profile image
Dhruv Malaviya •

DNS rebinding is the case that defeats hostname allowlists entirely, and it's worth adding because it survives a config that looks correct. evil.com can pass validation resolving to a public address and then resolve to 127.0.0.1 by the time the fetch happens. The check has to be on the resolved IP at connect time, not the hostname at parse time.

Related: naive IP blocklists get bypassed by encoding rather than cleverness. 2130706433, 0x7f.1, 0177.0.0.1, [::ffff:127.0.0.1] and IPv6 link-local all reach the same place. Blocking has to happen after normalisation, which most application-layer checks don't do.

The metadata endpoint is the headline but rarely the most valuable target. Internal admin panels, service registries and databases that trust the local network are usually worth more than a role credential the attacker still has to use.

Two provider-side mitigations that help in depth. IMDSv2 on AWS requires a PUT for a session token before any GET returns data, and an image optimizer won't do that , so enforcing IMDSv2 and disabling v1 closes this hole regardless of the wildcard. GCP's equivalent is requiring Metadata-Flavor: Google.

Worth stating plainly: the request going out is the damage. Whether the response parses as an image only decides whether the attacker reads the body or just learns the host exists.