CVE-2026-112949
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.
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
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 Medium
CVSS: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 Medium
CVSS: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 Medium
CVSS: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 Medium
CVSS: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 Medium
CVSS: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 Medium
CVSS: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
| Package | Affected | Fixed |
|---|---|---|
@vitest/mockernpm |
2.1.0-beta.7 up to 4.1.12 | 4.1.12 |
@vitest/mockernpm |
5.0.0-beta.1 up to 5.0.3 | 5.0.3 |
@vitest/mockerTuxCare: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/mockerTuxCare: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 |
vitestTuxCare: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
| Recorded | Changed | Sources |
|---|---|---|
| affected, aliases, facts | OSV | |
| first indexed | CVE 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
github.com/vitest-dev/vitest/security/advisories/GHSA-qxg6-89mg-rqv8
github.com/vitest-dev/vitest/commit/15cc006aa9f8683dc5f7e895d011fa7dc751ce35
github.com/vitest-dev/vitest/commit/b825313243dc55757e8d7c0f9e7a792135e13473
github.com/vitest-dev/vitest/releases/tag/v4.1.12
github.com/vitest-dev/vitest/releases/tag/v5.0.3
nvd.nist.gov/vuln/detail/CVE-2026-112949
github.com/advisories/GHSA-qxg6-89mg-rqv8
github.com/CVEProject/cvelistV5/tree/main/cves/2026/112xxx/CVE-2026-112949.json
github.com/vitest-dev/vitest
Left out here: the rest of 2 source descriptions. The JSON record has them.