Feb 26, 2026·6 min read·105 visits
Critical SSRF in esm.sh allows internal network access via DNS aliasing. The application validated URL strings instead of resolved IPs, enabling attackers to bypass 'localhost' blocks using domains like '127.0.0.1.nip.io'.
A critical Server-Side Request Forgery (SSRF) vulnerability in esm.sh allowed attackers to bypass string-based hostname validation using DNS aliases. By masking internal IP addresses behind innocent-looking domain names, attackers could trick the CDN into scanning local networks or retrieving cloud metadata. While a patch attempted to pin hosts during redirects, the fundamental flaw of validating hostnames before DNS resolution remains a classic example of 'checking the ID card but ignoring the face'.
In the modern web development ecosystem, 'no-build' CDNs like esm.sh are the new hotness. They allow developers to import npm packages directly into the browser as ES modules, handling all the transpilation and bundling on the fly. To achieve this magic, the service needs to fetch external resources. It acts as a highly specialized proxy, taking a URL, processing the content, and serving it back.
But here is the golden rule of web security: If you let a user tell your server where to go, they will eventually send it to hell.
The vulnerability lies in the /http(s) fetch route. This feature is designed to proxy and bundle external modules. From a hacker's perspective, this is not a 'feature'; it is an open invitation. It is a portal that accepts a URL and makes an HTTP request from the context of the server. If we can manipulate that request, we are no longer just visiting the website; we are inside their network.
The developers of esm.sh knew that SSRF was a risk. They implemented a safeguard function, likely something akin to isLocalhost, to forbid requests to loopback addresses. Their logic was simple: check the input URL.
If the hostname is "localhost", block it. If it is "127.0.0.1", block it. If it starts with "192.168.", block it.
This is the String Comparison Fallacy. It is like a bouncer at a club who has a list of banned names. If 'John Doe' is banned, but John shows up wearing a fake mustache and calls himself 'Juan Dough', the bouncer lets him in. The bouncer checks the name (the string), not the person (the IP address).
In the world of TCP/IP, computers do not care about strings; they care about IP addresses. Validation that happens before DNS resolution is effectively useless against a motivated attacker. We don't need to use the string "localhost"; we just need a string that becomes localhost when the server asks the DNS resolver.
Let's look at the fix introduced in commit 0593516c4cfab49ad3b4900416a8432ff2e23eb0. The developers realized that redirects were a major issue. Previously, you could point the validator to safe.com, which would then return a 302 Redirect to localhost. The initial check passes, and the HTTP client blindly follows the redirect into the danger zone.
To fix this, they introduced a map called allowedHosts and a CheckRedirect hook:
// Simplified view of the patch
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
// Only allow if the host was explicitly allowed during the initial check
if _, ok := allowedHosts[req.URL.Host]; !ok {
return fmt.Errorf("redirect host %s not allowed", req.URL.Host)
}
return nil
},
}This code (paraphrased for clarity) effectively pins the request to the original hostname. If you start at example.com, you must stay at example.com.
The Problem? This fixes redirect-based SSRF, but it arguably misses the forest for the trees. If the initial allowedHost is evil-alias.com, and evil-alias.com resolves to 127.0.0.1, the map entry is created for evil-alias.com. The redirect check is never triggered because there is no redirect—just a direct connection to a local IP disguised as a public domain.
Exploiting this requires zero coding and about 30 seconds of setup. We utilize a 'magic' DNS service like nip.io or sslip.io. These services automatically resolve subdomains to the IP address contained within the string.
The Attack Chain:
127.0.0.1.nip.io. To the esm.sh validator, this string does not equal "localhost" or "127.0.0.1". It looks like a valid external domain.GET https://esm.sh/http://127.0.0.1.nip.io:80/adminWhat happens inside the server?
if "127.0.0.1.nip.io" in blacklist. False. Request allowed.net/http client asks DNS: "Who is 127.0.0.1.nip.io?"127.0.0.1". The client connects to itself.> [!NOTE]
> Cloud environments are even juicier. Using 169.254.169.254.nip.io allows an attacker to steal AWS IAM credentials or instance metadata.
The impact of a blind SSRF like this ranges from "annoying" to "business-ending," depending on where the esm.sh instance is hosted.
If hosted in a Kubernetes cluster, an attacker can hit the Kubelet API, potentially dumping pod configurations or secrets. If hosted on AWS/GCP/Azure, the Instance Metadata Service (IMDS) is the primary target. While modern IMDS (IMDSv2) requires a token header, many legacy setups or misconfigured proxies (which might forward headers inadvertently) are still vulnerable to IMDSv1.
Furthermore, this turns the CDN into a port scanner. An attacker can fuzz ports on the local network (192.168.1.1.nip.io:22, :23, :25) and analyze the response times or error messages to map out the internal infrastructure. The server becomes a pivot point for further attacks.
The patch in version 137 adds friction by preventing redirects, which is good practice. However, the only true fix for SSRF is DNS Resolution at the Application Layer.
How to actually fix this:
Host header manually to the original hostname (to support virtual hosting).Until the validation logic moves from String-land to IP-land, bypasses will likely continue to surface.
CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
esm.sh esm-dev | <= 137 | 137 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-918 |
| Attack Vector | Network |
| CVSS v3.0 | 8.6 (High) |
| Attack Complexity | Low |
| Privileges Required | None |
| Exploit Maturity | Proof of Concept |
Server-Side Request Forgery (SSRF)
The Model Context Protocol (MCP) TypeScript SDK contains an uncontrolled resource consumption vulnerability in its HTTP server transport implementations and middleware adapters. Affected versions fail to enforce upper bounds on incoming HTTP POST request body sizes and JSON-RPC 2.0 batch array lengths. In addition, middleware wrappers for Express and Hono parse request bodies into memory prior to validating Host and Origin headers. Unauthenticated remote attackers can exploit these flaws to trigger Out-Of-Memory (OOM) process crashes or monopolize the single-threaded Node.js event loop.
An uncaught exception vulnerability (CWE-248) exists in the Node.js HTTP/1.1 client library undici, affecting versions 7.15.0 through 7.30.0 and 8.0.0 through 8.12.1. The flaw resides in the DecompressHandler interceptor (lib/interceptor/decompress.js), which processes compressed HTTP response streams using asynchronous Node.js zlib callbacks. When forwarding decompressed data chunks or stream completion events to downstream handlers, the interceptor fails to wrap the invocations of super.onResponseData() and super.onResponseEnd() in try...catch blocks. If a downstream interceptor or handler throws an exception during response processing—such as the built-in dump interceptor enforcing a maximum response size—the exception escapes the asynchronous zlib callback stack onto the top-level Node.js event loop, terminating the entire client process.
An uncontrolled resource consumption vulnerability in the WebSocket client implementation of Node.js undici allows a remote WebSocket server to trigger an out-of-memory (OOM) crash in the client process. The vulnerability stems from the failure of the incoming frame parser (ByteParser) to respect socket write backpressure when automatically emitting PONG responses to incoming PING control frames. By continuously streaming small 2-byte PING frames while refusing to read from the TCP connection, a malicious server forces the client to queue outbound PONG frames in the V8 heap without bound.
CVE-2026-87018 (GHSA-qrxm-fqhp-jwvm) is a high-severity Denial of Service (DoS) vulnerability in undici, the default HTTP/1.1 and HTTP/2 client for Node.js that backs the global fetch() implementation. The flaw stems from reachable assertions in undici's socket and session error handlers, which assume that TLS hostname verification errors (ERR_TLS_CERT_ALTNAME_INVALID) cannot occur after the initial connection establishment. By initiating a post-handshake TLS 1.2 renegotiation and presenting a certificate with a mismatched Subject Alternative Name (SAN), a remote HTTPS server can trigger this assertion and terminate the entire Node.js runtime process via an uncaught AssertionError.
FileBrowser Quantum prior to versions 1.5.2-stable and 2.0.1-beta contains an improper link resolution vulnerability (CWE-59) in its file indexing and retrieval subsystems. When processing file read, download, preview, media streaming, OnlyOffice integration, or public share requests, the application resolves symbolic links via Go's filepath.EvalSymlinks without verifying that the canonical target path remains within the configured source root and the user's assigned access scope. Consequently, authenticated users with read access or unauthenticated holders of public share links can read arbitrary server-accessible files outside their designated directory boundaries.
CVE-2026-116676 (GHSA-rjh2-4xgw-p2hj) is an OS command injection vulnerability (CWE-78) in Atlantis, an open-source Go application that automates Terraform pull request workflows via webhooks. From version 0.4.0 prior to version 0.47.1, Atlantis constructed Terraform and Conftest commands by joining argument slices into a single space-separated string and passing them to a shell interpreter (`sh -c`). Because user-controlled workspace names from `atlantis.yaml`, pull request comments, HCL configurations, and API requests were insufficiently validated against shell metacharacters, an authenticated contributor eligible to trigger a Terraform plan can execute arbitrary commands under the Atlantis service account.