# Supply Chain Security Analysis of a 9.5M-Install VS Code Extension

Markdown Preview Enhanced evaluated WaveDrom blocks as JS and trusted webview messages, letting a crafted Markdown file write arbitrary files and run OS commands.

- Payout: Research
- Severity: high
- Program: Markdown Preview Enhanced
- Found by (vendor credit): Neo by ProjectDiscovery
- Write-up by: Ritik Chaddha
- Disclosed: 2026-08-16
- CVE: CVE-2026-50733, CVE-2026-54566, CVE-2026-54701, CVE-2026-54702, CVE-2026-54703
- Write-up quality: 8/10
- Original: https://projectdiscovery.io/blog/a-9-5m-install-vs-code-extension-one-markdown-file-and-a-supply-chain-foothold
- Library page: https://bugbountylibrary.com/r/blog%3Aprojectdiscovery-io-blog-a-9-5m-install-vs-code-extension-one-markdown-file-and-a-supply-chain-foothold
- Tags: area:os domain:desktop impact:integrity impact:rce kind:research platform:research sev:high tech:nodejs technique:chained technique:source-review vuln:command-injection vuln:rce vuln:sandbox-escape vuln:xss

**Target:** VS Code extension webview and extension host (Markdown preview)
**Root cause:** WaveDrom block text was passed to window.eval, and the webview message dispatcher forwarded attacker-controlled command names/args to vscode.commands.executeCommand with an updateMarkdown command that wrote any file:// URI without containment. Separately, workspace .crossnote/config.js and parser.js were evaluated with vm.runInNewContext/sval sharing the host realm.
**Steps:**
1. Craft a Markdown file containing a WaveDrom block whose text is attacker JS; renderer runs `const content = window.eval(`(${text})`)`.
2. Injected JS recovers the webview API handle and sends postMessage with `command` and `args`.
3. Host dispatches `vscode.commands.executeCommand('_crossnote.' + message.command, ...message.args)` with no allowlist.
4. Reach `_crossnote.updateMarkdown` which does `vscode.workspace.fs.writeFile(vscode.Uri.parse(uri), Buffer.from(markdown))` on any file:// URI.
5. Write to ~/.ssh/authorized_keys (or .bashrc, git hooks) for persistence/RCE.
6. Alt path: place `.crossnote/config.js` with `var F = this.constructor.constructor; var p = F('return process')(); p.getBuiltinModule('child_process').execSync('id > /tmp/pwned.txt')`; open any Markdown in workspace.
7. Alt path: put a `<script>` in `.crossnote/head.html` that proxies acquireVsCodeApi before React/DOMPurify initializes, then reuse the dispatch→write chain.
**How found:** Multi-step source audit (217 steps, 322 tool calls) rather than grep-for-eval: traced how a plain Markdown input reaches eval, how the webview channel crosses into the host, and demonstrated the file write. Noted sanitizer allowlist preserved 'wavedrom'/'text/tikz' carriers, and that head.html injects into <head> before DOMPurify initializes.
**Exploitation:** Attacker ships a poisoned README.md or workspace config; victim opens it in preview. WaveDrom eval or head.html script executes in the webview, proxies acquireVsCodeApi, and postMessages a command to the host dispatcher, which invokes updateMarkdown to write arbitrary content to any file:// path such as ~/.ssh/authorized_keys, yielding persistent remote access and code execution on the developer machine.
**Escalation:** Webview XSS chained through unvalidated postMessage dispatch to an uncontained file write; file write to shell rc files, git hooks, build configs, or authorized_keys converts to host RCE and supply-chain compromise.
**Insight:** Sanitizer allowlists that preserve diagram/script-like content for renderers create a second-order sink when a later feature treats that preserved text as code; also, webview postMessage handlers that build command names by string concatenation turn any future XSS into host-level command invocation.

## Proof of concept (copied exactly from the original report)

**WaveDrom eval of Markdown block text**

```
const content = window.eval(`(${text})`);
```

**Unvalidated webview message to command dispatch**

```
previewPanel.webview.onDidReceiveMessage((message) => {
2 vscode.commands.executeCommand(
3 `_crossnote.${message.command}`, // attacker-controlled command name
4 ...message.args, // attacker-controlled arguments
```

