When Vite's HTML transformation is invoked manually via server.transformIndexHtml, the original request URL is passed in unmodified, and the html being transformed contains inline module scripts (<script type="module">...</script>), it is possible to inject arbitrary HTML into the transformed output by supplying a malicious URL query string to server.transformIndexHtml.
Only apps using appType: 'custom' and using the default Vite HTML middleware are affected. The HTML entry must also contain an inline script. The attack requires a user to click on a malicious URL while running the dev server. Restricted files aren't exposed to the attacker.
Fixed in vite@5.0.5, vite@4.5.1, vite@4.4.12
Suppose index.html contains an inline module script:
<script type="module">
// Inline script
</script>
This script is transformed into a proxy script like
<script type="module" src="/index.html?html-proxy&index=0.js"></script>
due to Vite's HTML plugin:
https://github.com/vitejs/vite/blob/7fd7c6cebfcad34ae7021ebee28f97b1f28ef3f3/packages/vite/src/node/plugins/html.ts#L429-L465
When appType: 'spa' | 'mpa', Vite serves HTML itself, and htmlFallbackMiddleware rewrites req.url to the canonical path of index.html,
https://github.com/vitejs/vite/blob/73ef074b80fa7252e0c46a37a2c94ba8cba46504/packages/vite/src/node/server/middlewares/htmlFallback.ts#L44-L47
so the url passed to server.transformIndexHtml is /index.html.
However, if appType: 'custom', HTML is served manually, and if server.transformIndexHtml is called with the unmodified request URL (as the SSR docs suggest), then the path of the transformed html-proxy script varies with the request URL. For example, a request with path / produces
<script type="module" src="/@id/__x00__/index.html?html-proxy&index=0.js"></script>
It is possible to abuse this behavior by crafting a request URL to contain a malicious payload like
"></script><script>alert('boom')</script>
so a request to http://localhost:5173/?%22%3E%3C/script%3E%3Cscript%3Ealert(%27boom%27)%3C/script%3E produces HTML output like
<script type="module" src="/@id/__x00__/?"></script><script>alert("boom")</script>?html-proxy&index=0.js"></script>
which demonstrates XSS.
vite dev middleware with appType: 'custom'
?%22%3E%3C/script%3E%3Cscript%3Ealert(%27boom%27)%3C/script%3E and navigate
vite dev (this shows that vanilla vite dev is not vulnerable, provided htmlFallbackMiddleware is used)
This will probably predominantly affect development-mode SSR, where vite.transformHtml is called using the original req.url, per the docs:
https://github.com/vitejs/vite/blob/7fd7c6cebfcad34ae7021ebee28f97b1f28ef3f3/docs/guide/ssr.md?plain=1#L114-L126
However, since this vulnerability affects server.transformIndexHtml, the scope of impact may be higher to also include other ad-hoc calls to server.transformIndexHtml from outside of Vite's own codebase.
My best guess at bisecting which versions are vulnerable involves the following test script
import fs from 'node:fs/promises';
import * as vite from 'vite';
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
</head>
<body>
<script type="module">
// Inline script
</script>
</body>
</html>
`;
const server = await vite.createServer({ appType: 'custom' });
const transformed = await server.transformIndexHtml('/?%22%3E%3C/script%3E%3Cscript%3Ealert(%27boom%27)%3C/script%3E', html);
console.log(transformed);
await server.close();
and using it I was able to narrow down to #13581. If this is correct, then vulnerable Vite versions are 4.4.0-beta.2 and higher (which includes 4.4.0).
{
"github_reviewed": true,
"nvd_published_at": "2023-12-04T23:15:27Z",
"github_reviewed_at": "2023-12-05T23:31:34Z",
"severity": "MODERATE",
"cwe_ids": [
"CWE-79"
]
}