Tech 6 min read

CVE-2025-24964 lets a malicious web page reach RCE via Vitest's WebSocket API

IkesanContents

TL;DR

What’s affected Projects using Vitest’s api option, or Vitest UI. Vulnerable: <=0.0.125, >=1.0.0 <1.6.1, >=2.0.0 <2.1.9, >=3.0.0 <3.0.5

What happens When a developer opens a malicious page in a browser, it connects to the Vitest API WebSocket listening locally, rewrites a test file, and triggers a rerun

What to do Update to a patched version:

  1. 1.x → 1.6.1 or later
  2. 2.x → 2.1.9 or later
  3. 3.x → 3.0.5 or later

This CVE works on localhost without any LAN exposure, so patching is the priority.


Vitest’s CVE-2025-24964 is the kind of vulnerability where a test runner’s convenience feature becomes a path straight to RCE (remote code execution) on the developer’s machine. With the Vitest API server running via vitest --ui or the api option, the developer opens a malicious web page. From that page, something connects to a local WebSocket like ws://localhost:51204/__vitest_api__ and calls into Vitest’s API.

In vulnerable versions, this WebSocket server didn’t validate the Origin header (the value indicating the connecting origin’s scheme, host, and port), and had no authorization mechanism. The browser’s same-origin policy doesn’t fully block WebSocket connections. With WebSocket, it’s the server’s responsibility to validate Origin at handshake time, and skipping that lets another site reach the locally listening port. This technique is called CSWSH (Cross-Site WebSocket Hijacking).

saveTestFile and rerun combine

The Vitest API has saveTestFile, which saves a test file, and rerun, which re-runs the tests of a specified file. Both are natural features for editing and re-running tests from the UI. But when an external site can call the WebSocket API, an attacker can write arbitrary JavaScript into an existing test file that Vitest recognizes, and have Vitest execute that file.

The PoC (proof-of-concept code) in the GitHub advisory demonstrates it by launching calc on Windows, but the calculator isn’t the point. Tests run as a Node.js process with the developer’s privileges, so a path opens to environment variables, config files inside the repo, SSH keys, and cloud CLI credentials. It’s closer to reality to see this as an RCE that happens on a local dev machine, not in CI.

flowchart TD
    A["Dev starts Vitest UI<br/>or enables api"] --> B["Vitest API WebSocket<br/>on localhost"]
    C["Open a malicious web page"] --> D["Browser connects to<br/>localhost API"]
    D --> E["saveTestFile rewrites<br/>a test file"]
    E --> F["rerun runs the<br/>rewritten test"]
    F --> G["Node.js code runs with<br/>dev privileges"]

    style G fill:#991b1b,color:#fff

It’s easy to miss that the attack requires a user action. This isn’t an RCE that’s over by firing a single request at Vitest across the network; it’s a sequence: “the Vitest API is listening,” “the developer opens the attack page,” “the browser reaches localhost.” Even so, the CVSS (Common Vulnerability Scoring System) score is high. The numbering authority GitHub (CNA) rates it 9.6 Critical, while NVD (NIST) rates it 8.8 High — the score splits by source. Some secondary databases show 9.7, but that’s not a primary-source value.

Fixed versions differ by release line

Fixed versions aren’t just a matter of looking at the 3.x line. The GitHub advisory’s fixed versions are split across the 1.x, 2.x, and 3.x lines.

LineVulnerable rangeFixed in
0.x<=0.0.125Not a directly continued line; migrate to a current line
1.x>=1.0.0 <1.6.11.6.1 or later
2.x>=2.0.0 <2.1.92.1.9 or later
3.x>=3.0.0 <3.0.53.0.5 or later

To check whether your project is affected, look at the resolved version in the lockfile, not the range spec in package.json.

pnpm why vitest
pnpm list vitest

For npm use npm ls vitest, for Yarn yarn why vitest. Even a project that only uses Vitest in CI is in scope if developers run vitest --ui locally.

CVE-2025-24963 in Browser Mode is a different entry point

On the same day, Vitest’s GHSA-8gvc-j273-4wm5 / CVE-2025-24963 was also published. This one isn’t RCE; it’s an issue where the __screenshot-error handler on the Browser Mode HTTP server returns arbitrary files. If you’ve explicitly exposed the server to the network with browser.api.host: true, file contents can be read remotely.

The two CVEs are both around Vitest’s development server, but the way you step on them differs.

CVETarget featureEntry pointImpact
CVE-2025-24964API WebSocketMalicious web page connects to localhost WebSocketRCE via test-file rewrite
CVE-2025-24963Browser Mode HTTP serverExternal HTTP reach via browser.api.host: true etc.Arbitrary file read

CVE-2025-24963’s fixed versions are 2.1.9+ for 2.x and 3.0.4+ for 3.x. But once you include CVE-2025-24964, the 3.x line needs 3.0.5 or later. If you look at only one of them and stop at 3.0.4, the RCE side remains.

Suspect configs that expose the dev server to the LAN

On the current Vitest docs, api and browser.api are separate config options. For ordinary local work, there’s little reason to expose these to the LAN or to 0.0.0.0. Projects that widened the host settings to allow connections from mobile devices, a browser on another machine, or outside a container suddenly gain reachable surface from outside in a dev-server CVE like this one.

Where to look: vitest.config.ts, vite.config.ts, the scripts in package.json, and CI startup commands. Check whether --ui, api, browser.api.host, host: true, 0.0.0.0, or fixed ports are specified. If you’re creating an “externally visible localhost” with Docker or devcontainer, trace how the browser-visible path connects to the listener inside the container.

In the sense of local reach for developer tools, it’s close to the piece I wrote earlier about commands reaching the local terminal from a compromised SSH host over VS Code Remote-SSH. That one was about the Remote-SSH trust model, this one is about Vitest’s missing Origin validation, but in both, a “convenience port that’s only meant for development” reaches your local privileges.

As a defense for npm packages as a whole, pnpm 11’s minimumReleaseAge and the age gate in Yarn/npm is another layer of defense. But this CVE isn’t about stepping on a malicious new package; it’s about launching an already-installed vulnerable version of Vitest. Lockfile pinning, dependency updates, and checking the dev server’s listen address are each separate layers.

References