From 7d8d29e257eeb098923b2436ea9ffd1411523510 Mon Sep 17 00:00:00 2001 From: Benedikt Meurer Date: Thu, 8 Oct 2026 09:46:00 +0200 Subject: [PATCH] Reject non-localhost Host headers on the DevTools JSON endpoint On Vite 5.x and 6.x, the synchronous body of plugin configureServer hooks executes before Vite registers hostCheckMiddleware (CVE-2025-24010). Because our endpoint middleware is registered in the synchronous body of configureServer (and cannot be moved to a post-hook without being intercepted by Vite's htmlFallbackMiddleware in SPA mode), validate the Host header directly against localhost (localhost, *.localhost, 127.0.0.0/8, [::1]) and reject non-localhost requests with HTTP 403 to prevent DNS rebinding attacks from reading the workspace root path and UUID. TAG=agy CONV=7701f19a-25ab-49bf-9131-831a137a7977 --- src/index.ts | 30 +++++++++++++++++++++++++++++- test/index.test.ts | 25 +++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 26c3ae6..cd95d3d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,6 +37,29 @@ interface DevToolsJSON { const ENDPOINT = '/.well-known/appspecific/com.chrome.devtools.json'; +const IPV4_LOOPBACK_RE = /^127(?:\.\d{1,3}){3}$/; + +function isLocalhost(hostHeader: string | undefined): boolean { + if (!hostHeader) { + return false; + } + try { + const url = new URL(`http://${hostHeader}`); + if (url.host !== hostHeader.toLowerCase()) { + return false; + } + const {hostname} = url; + return ( + hostname === 'localhost' || + hostname.endsWith('.localhost') || + hostname === '[::1]' || + IPV4_LOOPBACK_RE.test(hostname) + ); + } catch { + return false; + } +} + const plugin = (options: DevToolsJsonOptions = {}): Plugin => ({ name: 'devtools-json', enforce: 'post', @@ -96,7 +119,12 @@ const plugin = (options: DevToolsJsonOptions = {}): Plugin => ({ ); } - server.middlewares.use(ENDPOINT, async (_req, res) => { + server.middlewares.use(ENDPOINT, async (req, res) => { + if (!isLocalhost(req.headers.host)) { + res.writeHead(403); + res.end(); + return; + } // Determine the project root that will be reported to DevTools. const resolveProjectRoot = (): string => { if (options.projectRoot) { diff --git a/test/index.test.ts b/test/index.test.ts index de8100e..2eae9c0 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -138,5 +138,30 @@ describe('#VitePluginDevToolsJson', () => { await server.close(); delete process.env.DOCKER_DESKTOP; }); + + it('should reject non-localhost Host headers (DNS rebinding protection)', async () => { + const server = await createServer({ + plugins: [VitePluginDevToolsJson()], + server: { + port, + host: true, + }, + }); + + await server.listen(); + + const blockedResponse = await request(server.httpServer!) + .get('/.well-known/appspecific/com.chrome.devtools.json') + .set('Host', `attacker.example:${port}`); + expect(blockedResponse.status).to.equal(403); + expect(blockedResponse.text).to.equal(''); + + const allowedResponse = await request(server.httpServer!) + .get('/.well-known/appspecific/com.chrome.devtools.json') + .set('Host', `localhost:${port}`); + expect(allowedResponse.status).to.equal(200); + + await server.close(); + }); }); });