Feb 26, 2026·6 min read·45 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)
A state persistence vulnerability exists in Tornado's CurlAsyncHTTPClient component where pooled pycurl.Curl handles are reused across asynchronous requests without a complete state reset. Consequently, sensitive per-request configurations, such as client TLS certificates or proxy basic authentication credentials, persist on the shared handle. This behavior leads to subsequent requests leaking these credentials to unauthorized remote servers.
CVE-2026-48748 is a denial-of-service vulnerability in Netty's HTTP/3 codec (netty-codec-http3) occurring when QPACK dynamic tables are enabled but the blocked streams limit is not explicitly configured. A bug in limit checking and a memory leak in stream tracking allow unauthenticated remote attackers to exhaust the JVM heap memory and crash the server.
CVE-2026-50009 is a cryptographic design vulnerability in the Netty network application framework. Prior to version 4.2.15.Final, the framework's QUIC protocol implementation fails to cryptographically segregate the generated Connection IDs and the associated Stateless Reset Tokens. An on-path network attacker who sniffs traffic during a Connection ID rotation can extract secret token material from cleartext headers, enabling them to inject spoofed reset packets and terminate active connections.
A critical hostname verification bypass vulnerability exists in the Netty network application framework when configured as a TLS client. When a developer registers a custom plain X509TrustManager, Netty wraps it inside an X509TrustManagerWrapper to adapt it to the X509ExtendedTrustManager API. However, this wrapper discards the SSLEngine context, bypassing critical hostname checks. Because the wrapper is identified as an X509ExtendedTrustManager, standard cryptographic engines and Netty's OpenSSL wrappers do not re-wrap it, failing to execute any hostname validation. Consequently, clients silently accept certificates for any host, enabling unauthenticated Man-in-the-Middle (MitM) attacks.
An uncontrolled resource pre-allocation flaw in the Netty Redis codec module allows remote unauthenticated attackers to cause a denial of service (OutOfMemoryError) by sending a crafted Redis Serialization Protocol (RESP) array header.
CVE-2026-50020 is a medium-severity HTTP Request Smuggling/Response Smuggling vulnerability (CWE-444) within the Netty asynchronous network application framework. The flaw resides in Netty's HTTP codec implementation, specifically the HttpObjectDecoder class, which silently consumes arbitrary ISO control bytes preceding the first request line.