CVE202644578 Technical AnalysisNext.js SSRF Root Cause
Blogging

CVE202644578 Technical AnalysisNext.js SSRF Root Cause

CVE202644578 is a CWE918 server side request forgery caused by a missing guard in the Next.js WebSocket upgrade handler. The handler proxied any request whose parsed URL had a protocol, without checking that routing had finished and approved the proxy, which the normal HTTP handler always did. Selfhosted Node.js servers from 13.4.13 through 15.5.15 and 16.0.0 through 16.2.4 are affected. Fixes landed on 15.5.16 and 16.2.5.Most writeups of this vulnerability focus on impacting internal networks, cloud metadata, a CVSS score of 8.6. This technical analysis goes the other direction. It starts from the code, follows the data from the socket to the outbound connection and asks a reviewer question: what was the programmer assuming and where did that assumption fail? If you want the exploitation angle instead, our companion piece on the CVE 2026 44578 Next.js WebSocket SSRF vulnerability covers it.

BackgroundTwo Doors Into One Router

A selfhosted Next.js application started with the standard production command runs a Node.js HTTP server. That server emits two relevant events. The first is the ordinary request event, fired for every normal HTTP exchange. The second is the upgrade event, which Node fires when a client sends an Upgrade header and the server has a listener for it.

Next.js registers handlers for both. They live in the same file and share a great deal of logic, including a routing function that resolves where a given request should goto a page, to an asset, to a rewrite destination, or to an external proxy target. Because rewrites can point to external origins, the router legitimately knows how to proxy a request elsewhere. That legitimate capability is the thing an SSRF abuses.The design looked reasonable. The upgrade handler exists mainly so that development features such as hot reloading and applications that proxy WebSockets through rewrites, can function. It reuses the routing function and acts on the result. The flaw is in how much of the result it acted on.

How the Routing Function Reports Its Result

The router returns several fields. The ones that matter for this analysis are:

  • A matched output, indicating the request resolved to a concrete page or asset.
  • A parsed URL object, produced by parsing the request target.
  • A finished flag, indicating that routing completed.
  • A status code, set when routing produced a response such as a redirect.

The finished flag and the status code are the router's way of saying I have reached a verdict. A proxy decision is only trustworthy when routing has finished and no status code was set. The parsed URL by itself is just a parse result; it describes what the client typed, not what the router approved.

The Root CauseA Guard Present in One Handler and Absent in the Other

According to Hadrian technical writeup, the ordinary HTTP handler had always gated its proxy call on two conditions together routing finished and the parsed URL carried a protocol. The upgrade handler gated only on the second condition.

Think of it as a security checkpoint that checks two things at one door and one thing at the neighbouring door. Every traveller who would fail the second check simply walks through the neighbouring door. That is precisely what an attacker does here, choose the entrance with the weaker rule.This pattern is common enough to deserve a nameguard divergence. It appears whenever two code paths implement the same policy independently. The longer the paths coexist, the more likely a later edit updates one and forgets the other. Reviewers should treat every pair of sibling handlers as a diff to be read side by side.

Why the Parsed URL Had a Protocol at All

The next question is how an attacker influences the parsed URL. The answer is a feature of HTTP/1.1 that few developers use day to day.A request line normally contains a path, such as a route beginning with a slash. The protocol specification also allows an absolute form target, a complete URL including scheme and host. This form exists mainly for communication with forward proxies. Node.js does not rewrite the raw request target and is exposed unchanged on the request object.

The router begins by running the legacy url.parse function over that raw target. When the target is a plain path, the resulting object has no protocol. When the target is a full URL, the object has a protocol of http and a populated host. So a client that sends an absoluteform target can set the protocol field of the parsed URL and therefore can satisfy the only condition the upgrade handler tested.This is a textbook case of trusting a derived value that the attacker controls. The developer treated parsed URL has a protocol as a proxy for a rewrite configured by the site owner matched. In reality the field simply reflected the first line of the HTTP request.

The Slash Normalization Interaction

The analysis gets more interesting when we look at what the router did next. The routing function contains an early exit for targets containing repeated slashes or backslashes. The motivation is hygiene. Such paths can cause ambiguity and are redirected to a normalized form.A full URL contains a double slash right after the scheme. So the early exit fired and the router returned a result that said, in effect, finished, with a permanent redirect status.That is a verdict meaning do not proxy this; redirect the client.

The vulnerable upgrade handler never read that verdict. It had already seen a protocol on the parsed URL, so it went ahead and proxied. In other words, the routing function actually signalled that the request was not approved and the handler discarded the signal.

Two side effects of the normalization explain the shape of public exploit code:

  1. The scheme slashes are collapsed, which is why error logs from failed proxy attempts show a single slash after the scheme. Defenders can use that as a fingerprint.
  2. The explicit port is dropped, so the outbound connection falls back to the default HTTP port. This is why exploitation is effectively limited to port 80.

A security reviewer should note the irony. A defensive normalization step, added for correctness, both flagged the request as illegitimate and subtly reshaped it into something the proxy library could still use.

From Parse Result to Out bound Socket

Once the handler decides to proxy, it hands the request, the socket, the parsed URL and the initial buffered bytes to a proxy helper. That helper builds a target from the parsed URL and uses an HTTP proxy library to open a connection and pipe data in both directions.Because the helper reconstructs the target from the host in the parsed URL, the attackerchosen hostname survives. From the library perspective, this is an ordinary proxy request to a legitimate rewrite destination. It adds standard forwarding headers, connects, forwards the request and streams the response back over the original socket.

The flow end to end:

  1. The client connects and sends an absolute form request line with upgrade headers.
  2. Node raises the upgrade event instead of the request event.
  3. The router parses the raw target and produces a protocolbearing URL object.
  4. Routing exits early with a finished flag and a redirect status because of the double slash.
  5. The upgrade handler ignores both signals and proxies on the protocol alone.
  6. The proxy library connects to the attacker host on the default port and returns the response.

Nothing in this chain involves authentication, cookies, or middleware. Middleware does not run on this path, so the common defensive idea of putting a check in middleware would not have helped.

The Fix, Described Line by Line

The patch is small, which is typical of this bug class. The upgrade handler now reads the finished flag and the status code from the routing result alongside the fields it already used. The proxy call is allowed only when routing is finished and the parsed URL has a protocol. Even then, it proxies only when no status code was set. If a status code is present, the handler closes the socket instead.

The behaviour change in plain language:

  • BeforeIf the URL looks external, proxy it.
  • AfterIf routing finished, the URL is external and routing did not produce an error or redirect, proxy it. Otherwise, close the connection.

The fix was committed to the canary branch on May 4, 2026 and released on 15.5.16 and 16.2.5 on May 11, 2026. Some third party articles reference later patch numbers because subsequent releases in the same lines include other security fixes; the safe instruction is to upgrade to the latest patch release of your line and confirm against the official advisory.

Why This Bug Is Hard to Catch With Tools

Automated scanners struggle with this class of flaw for several reasons.

  • It is a missing condition, not a dangerous function. Static analysis looks for tainted data reaching a sink. Here, the sink (a proxy call) is legitimate and appears in both handlers. The defect is that one handler lacks a condition.
  • It lives in a framework, not in application code. Application level scanners never see it.
  • Exploitation needs protocollevel crafting. Many dynamic scanners send only wellformed paths.
  • Logging is thin. Upgrade requests do not complete a normal request/response cycle and many deployments never log them, so the activity is invisible in standard access logs.

A human reviewer, in contrast, can find it quickly by comparing the two handlers. That is why practising source code review pays off even in an era of automated tooling; the highest bugs are often the ones tools were never built to model.

A Reviewer Checklist Derived From This Bug

Use these questions whenever you review request routing code.

  1. Are there sibling handlers? List every place the same policy should apply, such as HTTP request, upgrade and any internal or development entry points.
  2. Do all siblings check the same conditions? Put the guards side by side.
  3. Which fields does the code trust? Identify whether each one is derived from attackercontrolled input.
  4. Does the code consume every output of the function it calls? Ignored return values and flags are a strong smell.
  5. Does the code accept absolute form or otherwise unusual request targets? Decide whether it should.
  6. What does the proxy helper do with the target? Check whether it revalidates the destination or trusts the caller.
  7. How would we notice abuse? If the answer is we would not, add logging.

These questions generalise well beyond Next.js. They apply to API gateways, custom reverse proxies and any service that forwards requests based on userinfluenced values. The same instinct, asking which input a decision really depends on, is the foundation of classic injection defence, which you can practise in the SQL injection lab.

Defensive Implications for Engineering Teams

A technical analysis is only useful if it changes how you build. Several defensive lessons follow directly from the mechanics.Upgrade first. The framework patch is the only complete fix. Reverseproxy rules and firewall controls reduce exposure, but they do not correct the faulty logic.Do not expose the Node.js process directly. A properly configured reverse proxy normally rejects absoluteform targets for ordinary requests, which removes the precondition for this attack.Constrain egress. Even a perfect patch leaves a long tail of future SSRF bugs. Deny outbound traffic from application servers to internal ranges and metadata addresses unless the application genuinely needs it.

Harden metadata. Require session oriented metadata access where your platform supports it. Because this specific flaw can only send GET requests, token based metadata flows block the most damaging outcome.Log the upgrade path. Capture request lines for upgrade events at the proxy or load balancer so that schemeprefixed targets are visible.For a broader catalogue of controls that make a single bug less catastrophic, read our guide to web security best practices. 

How This Fits the Wider CWE918 Picture

SSRF has been a staple of the OWASP Top 10 for years because it turns trusted servers into attacker controlled clients. Typical SSRF bugs involve an application fetching a URL supplied in a parameter, such as an image importer or a webhook tester. CVE202644578 is notable because there is no such feature. The fetch URL behaviour is a side effect of the routing layer rewrite support, exposed through a protocol feature that applications rarely think about.

That distinction matters for threat modelling. If your SSRF checklist only asks does any endpoint accept a URL parameter?, you will miss frameworklevel reachability like this. A better question is can any attackercontrolled byte influence the destination of any outbound connection our process makes? To practise answering it, work through the exercises in the web application hacking labs, where you can safely see how request manipulation changes server behaviour. 

Conclusion

CVE202644578 is a small bug with a large lesson. A single missing condition in one of two sibling handlers let a clientcontrolled parse result stand in for a routing verdict and a defensive normalization step accidentally reshaped the request into something the proxy still accepted. The result was an unauthenticated, readonly SSRF in the builtin Node.js server. The fix is a handful of lines, but the habits it teaches are broader: compare sibling code paths, never treat derived input as approval, consume every signal a function returns and log the protocol features your tooling ignores. Patch your selfhosted deployments, restrict their egress and use this case to sharpen the review skills that find the next one before an attacker does.You can find the full catalogue of training material on the AppSecMaster home page.

Frequently Asked Questions (FAQs)

What is the root cause of CVE202644578?

The WebSocket upgrade handler in the Next.js router proxied any request whose parsed URL contained a protocol, without confirming that routing had finished and approved the proxy. The ordinary HTTP handler performed that check; the upgrade handler did not.

Why does an absoluteform request line trigger the bug?

HTTP/1.1 allows a full URL as the request target and Node.js preserves it. When the router parses such a target, the resulting object carries an httpprotocol, which was enough to satisfy the only condition the vulnerable handler checked.

Why is exploitation limited to port 80?

URL normalization collapses the repeated slashes after the scheme and loses the explicit port. The outbound connection then falls back to the default HTTP port, so services listening on other ports are not directly reachable through this path.

How does the patch fix the vulnerability?

The upgrade handler now reads the routingfinished flag and the status code. It proxies only when routing is finished, the URL is external and no status code was set. Otherwise it closes the socket.

Does Next.js middleware protect against CVE202644578?

No. The vulnerable path is handled by the router before any application middleware runs, so middleware based checks do not intercept it. Upgrading to a patched release is the reliable fix.

Found this helpful? Share it!

Comments

0
No comments yet. Be the first!