Every CVE Search/ Get API key

CVE-2026-112949

Vitest: Code injection via unauthenticated manual mock registration

4.7 MediumFix available

Vitest is a Vite-native testing framework for JavaScript and TypeScript projects. From 2.1.0-beta.7 until 4.1.12 and 5.0.3, an unauthenticated non-browser WebSocket client that can reach a development server using interceptorPlugin can register a manual mock whose attacker-controlled url createManualModuleSource inserts unescaped into generated module source. When the developer browser loads the affected module, attacker-controlled JavaScript executes on the development-server origin. This issue is fixed in versions 4.1.12 and 5.0.3.

Attack profile

How easy it is to exploit

Attack vector
Network
Complexity
High
Privileges needed
None
User interaction
Required

What an attacker can affect

Confidentiality
Low
Integrity
Low
Availability
None

Exploit likelihood

0.34%Up 0.1 points since Oct 21, 2026
90%50%10%1%

What each source reports

CVE List Score used 4.7

Vitest: Code injection via unauthenticated manual mock registration

Vitest is a Vite-native testing framework for JavaScript and TypeScript projects. From 2.1.0-beta.7 until 4.1.12 and 5.0.3, an unauthenticated non-browser WebSocket client that can reach a development server using interceptorPlugin can register a manual mock whose attacker-controlled url createManualModuleSource inserts unescaped into generated module source. When the developer browser loads the affected module, attacker-controlled JavaScript executes on the development-server origin. This issue is fixed in versions 4.1.12 and 5.0.3.

Record
CVE-2026-112949
CVSS 3.1
4.7 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:L/A:N
Weakness
CWE-94, CWE-116, CWE-306
Updated
2026-11-02
EUVDEUVD-2026-105148 4.7

Vitest is a Vite-native testing framework for JavaScript and TypeScript projects. From 2.1.0-beta.7 until 4.1.12 and 5.0.3, an unauthenticated non-browser WebSocket client that can reach a development server using interceptorPlugin can register a manual mock whose attacker-controlled url createManualModuleSource inserts unescaped into generated module source. When the developer browser loads the affected module, attacker-controlled JavaScript executes on the development-server origin. This issue is fixed in versions 4.1.12 and 5.0.3.

Record
EUVD-2026-105148
CVSS 3.1
4.7 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:L/A:N
Updated
2026-11-02
GitHub AdvisoriesGHSA-qxg6-89mg-rqv8 4.7

Vitest: Code injection via unauthenticated manual mock registration

Summary

@vitest/mocker's interceptorPlugin accepts mock registrations on Vite's dev-server WebSocket with no authentication, and interpolates the mock's url into generated module source without escaping. A client that can reach the dev server can make it serve arbitrary JavaScript in place of any project module, which then runs in the developer's browser on the dev-server origin.

Same handler and same unauthenticated caller as GHSA-82fw-gwwq-j7x9; that fix (8ff9b9a9efca7c6cfd5243569440de8d7a33aec4, PR #10972) hardened only the redirect branch and left the manual branch reaching the unescaped generator.

Patches

Fixed in @vitest/mocker 5.0.3, released on 2026-09-30 (PR #11377), and backported to 4.1.12. createManualModuleSource now serializes the module URL and the export names with JSON.stringify, so a registered mock cannot change the generated code.

Projects that call mockerPlugin() or interceptorPlugin() directly need @vitest/mocker 4.1.12 or 5.0.3.

Details

packages/mocker/src/utils.ts:6-9 builds module source by string interpolation:

export function createManualModuleSource(moduleUrl: string, exports: string[], globalAccessor = '"__vitest_mocker__"'): string {
  const source = `
const __factoryModule__ = await globalThis[${globalAccessor}].getFactoryModule("${moduleUrl}");
`

moduleUrl lands inside a double-quoted JavaScript string with no escaping or validation.

packages/mocker/src/node/interceptorPlugin.ts:66-93 feeds it straight from the socket:

configureServer(server) {
  if (options.registerWebSocketEvents === false) { return }
  server.ws.on('vitest:interceptor:register', (event: MockedModuleSerialized) => {
    if (event.type === 'manual') {
      const module = ManualMockedModule.fromJSON(event, async () => {
        const keys = await getFactoryExports(event.url)
        return Object.fromEntries(keys.map(key => [key, null]))
      })
      registry.add(module)                       // no validation of `id` or `url`
    }
    else {
      if (event.type === 'redirect') { /* fs allowlist check added in #10972 */ }

event.id chooses which module is hijacked; event.url is what gets interpolated. The load hook (interceptorPlugin.ts:30-44) then returns createManualModuleSource(mock.url, exports, accessor) for that id.

With url = 'x"); globalThis.ARES_INJECTED = 1; void("' a request for /src/app.js is answered with valid, executable JavaScript:

const __factoryModule__ = await globalThis["__vitest_mocker__"].getFactoryModule("x"); globalThis.ARES_INJECTED = 1; void("");

getFactoryExports (interceptorPlugin.ts:103-117) also takes the export-name array from the same socket, and those names are interpolated unescaped too — but each name is emitted into __factoryModule__["X"], export { __0 as "X" } and resolvedModule["X"], and no payload tested parses in all three positions (all nine candidates were rejected by the parser). It is a hardening gap, not a second usable vector; url is the exploited one.

Who can reach the socket. Vite 8 answers HTTP 400 to any HMR upgrade carrying an Origin header it does not own, so a cross-origin browser page cannot open it — the PoC asserts this for https://evil.example and http://localhost:3000. The reachable attacker is a non-browser WebSocket client with access to the dev server's port, the same prerequisite under which GHSA-82fw-gwwq-j7x9 was published.

PoC

poc/mocker-manual-mock-injection/ — repro.sh (exit 0 = vulnerable), poc.mjs, expected.txt, env.txt. Everything lives under a fresh mktemp directory deleted on exit, the dev server binds to 127.0.0.1 on an ephemeral port, no external host is contacted, and the payload is only inspected as text — the script never evaluates it.

Environment: commit 9e1166959e14bd32298d8a0e85352431c769ec7a, @vitest/mocker 5.0.0, Node v22.23.1, Vite 8.0.11, ws 8.21.3, pnpm 11.24.0, Linux x86_64.

Record
GHSA-qxg6-89mg-rqv8
CVSS 3.1
4.7 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:L/A:N
Weakness
CWE-94, CWE-116, CWE-306
Updated
2026-11-02
NVD 4.7

Vitest is a Vite-native testing framework for JavaScript and TypeScript projects. From 2.1.0-beta.7 until 4.1.12 and 5.0.3, an unauthenticated non-browser WebSocket client that can reach a development server using interceptorPlugin can register a manual mock whose attacker-controlled url createManualModuleSource inserts unescaped into generated module source. When the developer browser loads the affected module, attacker-controlled JavaScript executes on the development-server origin. This issue is fixed in versions 4.1.12 and 5.0.3.

Record
CVE-2026-112949
CVSS 3.1
4.7 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:L/A:N
Weakness
CWE-94, CWE-116, CWE-306
Updated
2026-11-02
OSV 4.7

Vitest: Code injection via unauthenticated manual mock registration

Vitest is a Vite-native testing framework for JavaScript and TypeScript projects. From 2.1.0-beta.7 until 4.1.12 and 5.0.3, an unauthenticated non-browser WebSocket client that can reach a development server using interceptorPlugin can register a manual mock whose attacker-controlled url createManualModuleSource inserts unescaped into generated module source. When the developer browser loads the affected module, attacker-controlled JavaScript executes on the development-server origin. This issue is fixed in versions 4.1.12 and 5.0.3.

Record
CVE-2026-112949
CVSS 3.1
4.7 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:L/A:N
Weakness
CWE-116, CWE-306, CWE-94
Updated
2026-11-02
OSVGHSA-qxg6-89mg-rqv8 4.7

Vitest: Code injection via unauthenticated manual mock registration

Summary

@vitest/mocker's interceptorPlugin accepts mock registrations on Vite's dev-server WebSocket with no authentication, and interpolates the mock's url into generated module source without escaping. A client that can reach the dev server can make it serve arbitrary JavaScript in place of any project module, which then runs in the developer's browser on the dev-server origin.

Same handler and same unauthenticated caller as GHSA-82fw-gwwq-j7x9; that fix (8ff9b9a9efca7c6cfd5243569440de8d7a33aec4, PR #10972) hardened only the redirect branch and left the manual branch reaching the unescaped generator.

Patches

Fixed in @vitest/mocker 5.0.3, released on 2026-09-30 (PR #11377), and backported to 4.1.12. createManualModuleSource now serializes the module URL and the export names with JSON.stringify, so a registered mock cannot change the generated code.

Projects that call mockerPlugin() or interceptorPlugin() directly need @vitest/mocker 4.1.12 or 5.0.3.

Details

packages/mocker/src/utils.ts:6-9 builds module source by string interpolation:

export function createManualModuleSource(moduleUrl: string, exports: string[], globalAccessor = '"__vitest_mocker__"'): string {
  const source = `
const __factoryModule__ = await globalThis[${globalAccessor}].getFactoryModule("${moduleUrl}");
`

moduleUrl lands inside a double-quoted JavaScript string with no escaping or validation.

packages/mocker/src/node/interceptorPlugin.ts:66-93 feeds it straight from the socket:

configureServer(server) {
  if (options.registerWebSocketEvents === false) { return }
  server.ws.on('vitest:interceptor:register', (event: MockedModuleSerialized) => {
    if (event.type === 'manual') {
      const module = ManualMockedModule.fromJSON(event, async () => {
        const keys = await getFactoryExports(event.url)
        return Object.fromEntries(keys.map(key => [key, null]))
      })
      registry.add(module)                       // no validation of `id` or `url`
    }
    else {
      if (event.type === 'redirect') { /* fs allowlist check added in #10972 */ }

event.id chooses which module is hijacked; event.url is what gets interpolated. The load hook (interceptorPlugin.ts:30-44) then returns createManualModuleSource(mock.url, exports, accessor) for that id.

With url = 'x"); globalThis.ARES_INJECTED = 1; void("' a request for /src/app.js is answered with valid, executable JavaScript:

const __factoryModule__ = await globalThis["__vitest_mocker__"].getFactoryModule("x"); globalThis.ARES_INJECTED = 1; void("");

getFactoryExports (interceptorPlugin.ts:103-117) also takes the export-name array from the same socket, and those names are interpolated unescaped too — but each name is emitted into __factoryModule__["X"], export { __0 as "X" } and resolvedModule["X"], and no payload tested parses in all three positions (all nine candidates were rejected by the parser). It is a hardening gap, not a second usable vector; url is the exploited one.

Who can reach the socket. Vite 8 answers HTTP 400 to any HMR upgrade carrying an Origin header it does not own, so a cross-origin browser page cannot open it — the PoC asserts this for https://evil.example and http://localhost:3000. The reachable attacker is a non-browser WebSocket client with access to the dev server's port, the same prerequisite under which GHSA-82fw-gwwq-j7x9 was published.

PoC

poc/mocker-manual-mock-injection/ — repro.sh (exit 0 = vulnerable), poc.mjs, expected.txt, env.txt. Everything lives under a fresh mktemp directory deleted on exit, the dev server binds to 127.0.0.1 on an ephemeral port, no external host is contacted, and the payload is only inspected as text — the script never evaluates it.

Environment: commit 9e1166959e14bd32298d8a0e85352431c769ec7a, @vitest/mocker 5.0.0, Node v22.23.1, Vite 8.0.11, ws 8.21.3, pnpm 11.24.0, Linux x86_64.

Record
GHSA-qxg6-89mg-rqv8
CVSS 3.1
4.7 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:L/A:N
Weakness
CWE-116, CWE-306, CWE-94
Updated
2026-11-02
OSVCLSA-2026-1793114894 No score

TuxCare security update for @vitest/mocker (1 CVE)

TuxCare rebuilt @vitest/mocker to address 1 CVE. Fixed in 3.2.4-tuxcare.5.

Record
CLSA-2026-1793114894
Updated
2026-11-02
OSVCLSA-2026-1793115217 No score

TuxCare security update for @vitest/mocker (1 CVE)

TuxCare rebuilt @vitest/mocker to address 1 CVE. Fixed in 4.0.18-tuxcare.5.

Record
CLSA-2026-1793115217
Updated
2026-11-02
OSVCLSA-2026-1793811643 No score

TuxCare security update for vitest (1 CVE)

TuxCare rebuilt vitest to address 1 CVE. Fixed in 2.1.9-tuxcare.2.

Record
CLSA-2026-1793811643
Updated
2026-11-04

Affected products

Affected packages

Affected packages
PackageAffectedFixed
@vitest/mocker
npm
2.1.0-beta.7 up to 4.1.12
4.1.12
@vitest/mocker
npm
5.0.0-beta.1 up to 5.0.3
5.0.3
@vitest/mocker
TuxCare:npm
3.2.4-tuxcare.0 up to 3.2.4-tuxcare.5, plus listed versions
3.2.43.2.4-tuxcare.13.2.4-tuxcare.23.2.4-tuxcare.33.2.4-tuxcare.4
3.2.4-tuxcare.5
@vitest/mocker
TuxCare:npm
4.0.18-tuxcare.0 up to 4.0.18-tuxcare.5, plus listed versions
4.0.184.0.18-tuxcare.14.0.18-tuxcare.24.0.18-tuxcare.34.0.18-tuxcare.4
4.0.18-tuxcare.5
vitest
TuxCare:npm
2.1.9-tuxcare.0 up to 2.1.9-tuxcare.2, plus listed versions
2.1.92.1.9-tuxcare.1
2.1.9-tuxcare.2

History

History
RecordedChangedSources
affected, aliases, factsOSV
first indexedCVE List, EUVD, GitHub Advisories, NVD, OSV

Dates are when Every CVE recorded each change. Upstream dates may differ.

In short

CVE-2026-112949 is a medium vulnerability (CVSS 4.7) in Vitest-dev Vitest, published October 20, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.34%, the 27th percentile. Fixed in @vitest/mocker 4.1.12.

References

This page doesn't list everything

Left out here: the rest of 2 source descriptions. The JSON record has them.

MoveEnter Open