Skip to content

Security1 publisher3 min readPublished

Five CVEs in a Markdown extension with 9.5 million installs, and none of it in your SBOM

ProjectDiscovery says an audit of Markdown Preview Enhanced turned an ordinary repository file into arbitrary file write. Editor extensions run with developer privileges and go uninventoried.

The Watch · Security desk

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

What happened

  • ProjectDiscovery audited the VS Code extension Markdown Preview Enhanced, which has roughly 9.5 million installs, using its tool Neo.
  • The audit found five CVEs across two attack surfaces, and all five are now fixed.
  • Code editor extensions auto-update and run with the user's privileges on the machine that holds their source code, SSH keys and publishing credentials, but they rarely show up in a software bill of materials.
  • Before the fix, the Markdown Preview Enhanced WaveDrom renderer took the raw text of a WaveDrom block and evaluated it as JavaScript with the call const content = window.eval(`(${text})`).
  • The evaluated text comes straight from the Markdown file, so opening a crafted file and rendering the preview reaches the path, with no code-chunk opt-in and no setting to enable.

Compiled by The WatchSomething wrong?How this is made

Why it matters

ProjectDiscovery has published an audit of Markdown Preview Enhanced, a VS Code extension with roughly 9.5 million installs, reporting five CVEs across two attack surfaces, all now fixed [1] [2]. The reason to care is not the Markdown parser: editor extensions auto-update and run with a developer's privileges on the machine that holds the source code, the SSH keys and the publishing credentials, and they rarely appear in a software bill of materials [3].

The first chain starts in the renderer. Before the fix, the WaveDrom diagram renderer took the raw text of a WaveDrom block and evaluated it as JavaScript through `window.eval` [4]. That text comes straight from the Markdown file, so opening a crafted file and rendering the preview is enough; there is no code-chunk opt-in and no setting to enable [5].

The extension did sanitize. After an earlier XSS fix it added Cheerio and DOMPurify passes, but diagram renderers need their data preserved, so the sanitizer kept a small allowlist of script-like diagram types, `wavedrom` and `text/tikz`, and let the WaveDrom carrier through [6]. Sanitization held at one layer; a later feature treated the preserved content as code anyway [7].

Execution inside a preview is, on its own, webview XSS [8]. The damage is at the second boundary. VS Code isolates webviews from the extension host, and the two talk via `postMessage`, which is only safe if the host treats those messages as untrusted [9]. This host did not: it built both the command name and its arguments from the incoming message and handed them to `vscode.commands.executeCommand` [10]. One reachable command, `_crossnote.updateMarkdown`, parsed a caller-supplied URI and wrote content to it with no workspace containment check, which means any `file://` URI and any content [11]. Four links: Markdown reaches `eval`, the injected script recovers the webview API handle the preview already holds, that handle reaches command dispatch, and dispatch lands on arbitrary file write [12]. That is CVE-2026-50733, fixed in v0.8.28 [13].

The two pieces of shared plumbing are independently dangerous, because any JavaScript running in the webview can reach them, not only WaveDrom [14]. They were filed separately as CVE-2026-54702 and CVE-2026-54703 [15]. An unsanitized HTML injection supplied a second XSS entry point, CVE-2026-54701 [16]. And two entry points needed no Markdown at all: the extension loads `.crossnote/config.js` and `.crossnote/parser.js` from any workspace that contains them, and a VM sandbox escape gave a workspace configuration file direct OS command execution, CVE-2026-54566, fixed in v0.8.29 [17] [18].

That is the part that should change how you think about your dependency inventory. The delivery vehicle here is not a poisoned npm package with a version string you can diff; it is a file sitting in a repository you cloned, executed by a tool that updated itself last week and that no manifest lists [3] [19]. The fixes span two releases, so anything below v0.8.29 is missing at least one of the five disclosed patches [20].

Worth tracking: the minimum extension version present across developer laptops and CI images, and whether your asset inventory has a field for editor extensions at all. The bug class also generalises beyond this vendor, since it is host-side code trusting messages from a sandbox it deliberately created [9]; any extension that dispatches commands from `postMessage` payloads is running the same design.

Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories