GHSA-p2v6-84h2-5x4r esm.sh has SSRF localhost/private-network bypass in `/http(s)` module route
SummaryAn SSRF vulnerability (CWE-918) exists in esm.sh’s `/http(s)` fetch route.  
The service tries to block localhost/internal targets, but the validation is based on hostname string checks and can be bypassed using DNS alias domains (for example, `127.0.0.1.nip.io` resolving to `127.0.0.1`).  
This allows an external requester to make the esm.sh server fetch internal localhost services.  
Severity:  High (depending on deployment network exposure).DetailsThe vulnerable flow starts at the route handling user-controlled remote URLs:`server/router.go:532`Accepts paths beginning with `/http://` or `/https://`.The internal-target block is string-based:`server/router.go:545`Localhost detection itself is limited to hostname patterns:`server/utils.go:72``isLocalhost(...)` checks values like `localhost`, `127.0.0.1`, and `192.168.*`.It does **not** validate the resolved destination IP after DNS resolution.Fetch proceeds with host-string allowlisting:`server/router.go:595-596``allowedHosts[modUrl.Host] = struct{}{}` then `fetch.NewClient(...allowedHosts)``internal/fetch/fetch.go:49`Host allowlist compares host strings, not resolved IP class.Because validation is based on host strings and not on resolved destination IP ranges, domains that resolve to loopback/private IP can bypass protections.PoCReproduction tested on local Docker deployment.Run esm.sh:Run an internal localhost-only test service (`secret` response) in the same network namespace:Internal network test server code (app.py):Run the internal Python server container (same network namespace as `esmsh-5558`):Since this server has no Docker port forwarding configured, it is not reachable from outside and is only accessible from the esmsh-5558 container connected on the same network.Since both were running on localhost, I tested it through a Cloudflared tunnel to simulate external access.Trigger SSRF from outside via esm.sh endpoint:127.0.0.1 is blocked,
<img width="1206" height="322" alt="image" src="https://github.com/user-attachments/assets/054a7675-5b9e-461a-bb55-9ec7a2b2f43b" />but 127.0.0.1.nip.io bypasses the filter. 
<img width="1210" height="336" alt="image" src="https://github.com/user-attachments/assets/95b991b1-ff93-495f-b624-458dd48fd5ff" />This confirms external requesters can fetch internal localhost service content through esm.sh.ImpactThis is a Server-Side Request Forgery vulnerability (CWE-918).Impacted:Any esm.sh deployment exposing the `/http(s)` route to untrusted users.Environments where internal services are reachable from the esm.sh server/container network.Potential consequences:Access to localhost/internal HTTP services not intended for public access.Internal service discovery/probing through the server.Exposure of sensitive internal endpoints (deployment-dependent, e.g., metadata/internal admin APIs).The exploit surface is extension-limited in this route (e.g., ".js", ".ts", ".mjs", ".mts", ".jsx", ".tsx", ".cjs", ".cts", ".vue", ".svelte", ".md", ".css"), so it is not a universal arbitrary-file fetch primitive.Even with that limitation, **attackers can still verify whether internal HTTP services exist** and **retrieve internal JavaScript/Markdown resources (and similar allowed extension content) when present.**If the internal server is implemented with Apache Tomcat, it may interpret everything after ; as a path parameter in a request such as /asdf/;asdf=a.js. As a result, **it could be possible to bypass extension checks while still receiving the response from the intended path.**
CVSS v3.0 score: 8.6