What happens
/_next/image validates the url query parameter, then follows redirects without re-checking where they lead.
In packages/cloudflare/src/cli/templates/images.ts, validateUrlQueryParameter checks the scheme and calls hasRemoteMatch(__IMAGES_REMOTE_PATTERNS__, parsedURL). Both run once, against the original URL. fetchWithRedirects then reads the Location header and recurses into itself with the new target, and hasRemoteMatch is not called again. Grepping the file, hasRemoteMatch appears in exactly two places: its definition and that single call site.
So from hop one onwards there is no scheme check and no allow-list check. An allowlisted host that returns a redirect decides where the loader goes next, including to a literal loopback, link-local or RFC1918 address.
Reproduced by reading the code at @opennextjs/cloudflare@1.20.2 and on main; the shape is unchanged in both.
Why not simply re-run the allow list
Re-running hasRemoteMatch on every hop would break a legitimate and common pattern: an allowlisted CDN redirecting to a signed URL on a sibling domain that is not itself in remotePatterns. That seemed likely to break real deployments.
Next's own optimizer does not re-apply remotePatterns per hop either. What it does do is re-enter the whole of fetchExternalImage on each hop, so its private-address guard runs again every time. That guard uses Node's DNS module, which has no equivalent in the Workers runtime, so it cannot be ported directly.
Suggested shape
Check each redirect target for scheme and for a literal non-routable address before following it. That is coarse, since without DNS a hostname cannot be resolved to find out where it points, but it closes the unambiguous cases and needs no new configuration.
There is already a // TODO: Add dangerouslyAllowLocalIP support in fetchWithRedirects, so this is adjacent to something the code already anticipates.
PR: #1338
Note on scope
This was originally raised privately. Cloudflare's security team reviewed it and concluded it does not demonstrate a security-boundary violation, so it is filed here as a hardening item at their suggestion. Worth stating plainly so nobody reads more into it than that: on a standard deployment fetch() goes to the public internet, private network access requires separate bindings, and the response still has to pass image content-type detection.
What happens
/_next/imagevalidates theurlquery parameter, then follows redirects without re-checking where they lead.In
packages/cloudflare/src/cli/templates/images.ts,validateUrlQueryParameterchecks the scheme and callshasRemoteMatch(__IMAGES_REMOTE_PATTERNS__, parsedURL). Both run once, against the original URL.fetchWithRedirectsthen reads theLocationheader and recurses into itself with the new target, andhasRemoteMatchis not called again. Grepping the file,hasRemoteMatchappears in exactly two places: its definition and that single call site.So from hop one onwards there is no scheme check and no allow-list check. An allowlisted host that returns a redirect decides where the loader goes next, including to a literal loopback, link-local or RFC1918 address.
Reproduced by reading the code at
@opennextjs/cloudflare@1.20.2and onmain; the shape is unchanged in both.Why not simply re-run the allow list
Re-running
hasRemoteMatchon every hop would break a legitimate and common pattern: an allowlisted CDN redirecting to a signed URL on a sibling domain that is not itself inremotePatterns. That seemed likely to break real deployments.Next's own optimizer does not re-apply
remotePatternsper hop either. What it does do is re-enter the whole offetchExternalImageon each hop, so its private-address guard runs again every time. That guard uses Node's DNS module, which has no equivalent in the Workers runtime, so it cannot be ported directly.Suggested shape
Check each redirect target for scheme and for a literal non-routable address before following it. That is coarse, since without DNS a hostname cannot be resolved to find out where it points, but it closes the unambiguous cases and needs no new configuration.
There is already a
// TODO: Add dangerouslyAllowLocalIP supportinfetchWithRedirects, so this is adjacent to something the code already anticipates.PR: #1338
Note on scope
This was originally raised privately. Cloudflare's security team reviewed it and concluded it does not demonstrate a security-boundary violation, so it is filed here as a hardening item at their suggestion. Worth stating plainly so nobody reads more into it than that: on a standard deployment
fetch()goes to the public internet, private network access requires separate bindings, and the response still has to pass image content-type detection.