CVE-2026-112948 is a Moderate (CVSS 6.5) flaw in Vitest 5 where the test runner's own HTTP servers stop applying Vite's file deny list. While the UI, Browser Mode or the API server runs, files such as .env, private keys, .npmrc, .git and Vitest's API token can be served. Upgrade to vitest 5.0.5 (or vite-plus 1.3.0), then decide what to rotate.
This is not a production bug. The advisory is explicit that only the machine running Vitest is exposed, and only while it runs one of a few specific setups. Plain vitest and vitest run open no port and are not affected. That narrows the blast radius a lot, but it also makes the bug easy to dismiss. Developer laptops, dev containers, cloud workspaces and CI runners are exactly where .env files and registry tokens live, and those are the files the deny list exists to protect.
This post covers the facts from the advisory, the fix commit and the package registry, the exact files and conditions, how to find every affected copy across repos and CI images, how to upgrade each package manager, a decision tree for what to rotate, a safe Vitest server configuration, and what would have helped.
What happened
The short version: a hardening change in the Vitest 5 betas added Vitest's new API token file to Vite's server.fs.deny list. The way it was added had the side effect of dropping Vite's default deny entries on one kind of server and never applying the token entry on the other. A follow-up pull request fixed both, and it shipped in vitest 5.0.5 on 15 October. The advisory was published the next day, reached NVD on 20 October and the reviewed GitHub Advisory Database on 21 October.
Timeline
Times are UTC where the source gives a timestamp. Where a source gives only a date, the table says so.
| When (2026) | Event | Source |
|---|---|---|
| 19 Jun (date only) | Pull request #10583, "harden UI API access", is merged. It adds token authentication for the Vitest UI, stores a stable token in a file named .vitest-secret-token in the user data directory, and adds that file to Vite's fs.deny. | vitest-dev/vitest PR #10583 |
| 6 Jul, 06:54:08 | vitest@5.0.0-beta.6 is published to npm. The advisory names this as the first affected version. It is the first 5.x release after the 19 June merge (beta.5 shipped 15 June). | npm registry time data; GitHub advisory |
| 3 Sep, 12:24:30 | vitest@5.0.0 stable is published. | npm registry |
| 22 Sep, 13:57:15 | vite-plus@1.0.0-rc.0 is published; the advisory names it as the first affected vite-plus version. | npm registry; GitHub advisory |
| 12 Oct, 09:47:02 | vitest@5.0.4 is published, the last affected stable release. | npm registry |
| 14 Oct (date only) | Pull request #11578, "keep vite default fs.deny with api token file", is approved and merged by a maintainer as commit 28dc1ed. | vitest-dev/vitest PR #11578 |
| 15 Oct, 14:29:01 | vitest@5.0.5 is published to npm with the fix. The GitHub release page lists the change under bug fixes, with no security section. | npm registry; v5.0.5 release notes |
| 15 Oct, 14:51:24 | vitest@4.1.12 is published on the 4.x line, which the advisory says is not affected. | npm registry |
| 16 Oct, 05:42:37 | vite-plus@1.3.0 is published, bundling vitest 5.0.5. | npm registry; vite-plus v1.3.0 release notes |
| 16 Oct (date only) | The Vitest repository publishes advisory GHSA-p458-7748-mcp3, the same day as two other Moderate Vitest advisories. | Vitest security advisories page |
| 20 Oct, 16:17:05 | NVD publishes CVE-2026-112948 with status "Awaiting Analysis". A CISA ADP SSVC entry records exploitation as "none". | NVD API |
| 21 Oct, 19:57:00 | The reviewed entry appears in the GitHub Advisory Database (OSV mirror timestamp). | OSV.dev |
| 22 Oct, 09:57:57 | The advisory is posted to Hacker News as "Vitest Dev Server Secret Leak". | Hacker News search API |
Two things stand out. First, the fix was public for six days before the reviewed advisory landed, and the release notes do not flag it as security relevant. Teams that only upgrade on security alerts were exposed longer than teams that track routine patch releases. Second, the window of affected releases runs from early July (betas) through mid October, about three months of stable 5.0.x adoption from 3 September.
What changed since first reports
The lead that reached our desk on 22 October called this a "GitHub advisory, 22 Oct" story. The primary sources show the fix shipped on 15 October, a week before most people heard about it, and that the reviewed GitHub Advisory Database entry lists @vitest/browser, @vitest/ui and vite-plus but not vitest itself, even though the fix ships in vitest. NVD has not scored the CVE; the only CVSS score comes from GitHub as the CNA. Details are in "Where sources differ" below.
Technical details
Every value in this section comes from the sources listed at the end. Where sources disagree, the disagreement is stated.
Identifiers and scores
| Field | Value | Source |
|---|---|---|
| CVE | CVE-2026-112948 | GitHub advisory, NVD |
| GHSA | GHSA-p458-7748-mcp3 | GitHub advisory |
| Advisory title | "Servers misapply server.fs.deny, serving .env, private keys, .git, and the API token file" | Vitest repository advisory |
| Severity (GitHub, CNA) | Moderate, CVSS 3.1 base score 6.5 | GitHub advisory; NVD lists it as "MEDIUM" from security-advisories@github.com |
| CVSS 3.1 vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N | GitHub advisory, NVD, OSV |
| NVD's own score | None yet; status "Awaiting Analysis" | NVD API, last modified 20 Oct 19:56:45 |
| CISA ADP SSVC | Exploitation: none; Automatable: no; Technical impact: partial | NVD API |
| EPSS | 0.406% (34th percentile) at the time of our fetch | GitHub Advisory Database page |
| CWE | CWE-200 (Exposure of Sensitive Information to an Unauthorized Actor), CWE-552 (Files or Directories Accessible to External Parties) | GitHub advisory, NVD |
| Credits | Reporter: sondt99. Finder: tomohiro86. The global database page also lists tikket1. | Repository advisory; GitHub Advisory Database page |
| Report note | The repository advisory notes the report was submitted with AI assistance. | Repository advisory |
Reading the vector in plain terms: reachable over a network, low complexity, no privileges, but it needs a user action (the developer has to be running an affected server, and in the browser scenario has to visit a page), with high confidentiality impact and no integrity or availability impact. The score reflects disclosure only. The advisory adds that with the token, privileged API access becomes possible, which is not captured in the base score.
Affected and fixed versions
| Package | Affected | Fixed | Notes | Source |
|---|---|---|---|---|
vitest | >= 5.0.0-beta.6, < 5.0.5 | 5.0.5 | The fix ships here. Listed in the repository advisory and NVD; not listed as a separate row in the reviewed GitHub Advisory Database entry. | Repository advisory, NVD, OSV note |
@vitest/ui | >= 5.0.0-beta.6, < 5.0.5 | 5.0.5 | Pins vitest as a peer dependency at the same version, so you must upgrade both. | GitHub advisory, OSV |
@vitest/browser | >= 5.0.0-beta.6, < 5.0.5 | 5.0.5 | Same peer pin. | GitHub advisory, OSV |
vite-plus | >= 1.0.0-rc.0, < 1.3.0 | 1.3.0 | Bundles vitest; 1.3.0 depends on vitest 5.0.5. | GitHub advisory, npm registry |
vitest 4.x and earlier | Not affected | n/a | The advisory says these versions neither persist an API token nor override server.fs.deny. Latest 4.x on npm is 4.1.12. | Repository advisory, npm registry |
Affected vitest releases, from the npm registry: 5.0.0-beta.6, 5.0.0-beta.7, 5.0.0-rc.1 through 5.0.0-rc.4, 5.0.0, 5.0.1, 5.0.2, 5.0.3 and 5.0.4. Affected vite-plus releases: 1.0.0-rc.0, 1.0.0, 1.1.0 and 1.2.0. At the time of writing the latest dist tag for vitest is 5.0.5 and for vite-plus is 1.3.0. A 5.1.0-beta.1 prerelease was published on 19 October, after the fix; there is no 5.1.0-beta.0 on the registry.
Exactly which files are exposed
The files are the ones on Vite's default deny list plus Vitest's token file. Vite's documentation (v8.3.4 at the time of our fetch) gives the default server.fs.deny as:
['.env', '.env.*', '*.{crt,pem,key,p12,pfx,cer,der}', '.npmrc', '.yarnrc.yml', '**/.git/**']
The fix commit hard-codes the same six patterns as a copy of Vite's newest defaults and appends the token file. So the exposure list is:
| Pattern | What typically lives there |
|---|---|
.env, .env.* | API keys, database URLs, cloud credentials, third-party tokens loaded by the app or tests |
*.crt, *.pem, *.key, *.p12, *.pfx, *.cer, *.der | TLS private keys and certificates for local HTTPS, client certificates, signing keys, service account keys saved as PEM |
.npmrc | npm registry auth tokens, including private registry tokens |
.yarnrc.yml | Yarn Berry registry auth tokens (npmAuthToken) |
**/.git/** | Repository history and config, which can include credentials embedded in remote URLs |
.vitest-secret-token | The token that authenticates the Vitest UI, attachments and the API WebSocket |
Only files the server can reach are at risk. On the project servers, that means files under the paths Vite allows (by default the detected workspace root, per Vite's docs). The token file is normally in the per-user data directory, outside server.fs.allow, so it is only reachable in a fallback case described next.
Conditions, by server
The advisory includes a table of which server leaks what. Restated:
| How you run Vitest | Listens on a port? | Vite default deny files | .vitest-secret-token |
|---|---|---|---|
Plain vitest or vitest run | No | Not reachable | Not reachable |
--ui, or api enabled (root server) | Yes | Denied | Served, only in the fallback location |
| Browser Mode, single project (root server) | Yes | Denied | Served, only in the fallback location |
Browser Mode with test.projects (each project's server) | Yes | Served | Denied |
The token fallback location is <workspaceRoot>/node_modules/.vitest/.vitest-secret-token. According to the advisory, Vitest falls back to it when the per-user data directory is not writable, for example in a container without a writable HOME. That is a common CI and dev container setup.
The advisory also states that users who enable api without installing @vitest/ui or @vitest/browser are affected and should upgrade vitest.
Who can reach the server
The advisory describes two routes, at a level that is useful for defenders:
- A web page in the developer's browser, only in Browser Mode with
test.projects. Because the project server returns a.envfile as JavaScript, a page the developer visits while the server runs can load it as a classic script and read dotenv lines whose values happen to be valid JavaScript. The advisory lists real limits: a syntax error anywhere in the file (a#comment line, anexportprefix) stops the script entirely; unquoted values stop it at that line, so only earlier lines leak; cross-originfetchand XHR reads are blocked by Vite's default CORS policy; theHostcheck blocks DNS rebinding; and recent Chrome versions may first ask for Local Network Access permission. The token cannot be read this way. The advisory notes one gap: Vite's default CORS policy allows localhost origins, so a page served from another local dev server could read full files. - Anyone who can reach the port because of local access or a developer-set exposure, such as
api.host,browser.api.host, port forwarding, or a published container port. In that case full file contents are readable over plain HTTP, including the token in the fallback location.
What the token unlocks
Per the advisory, the token authenticates the UI, the /__vitest_attachment__ endpoint and the API WebSocket. That RPC can write and rerun test files when api.allowWrite or api.allowExec is enabled. Vitest's docs say both default to true when the server is not exposed to the network and to false when host is set to anything other than localhost or 127.0.0.1. So the most damaging combination, an exposed server with write and exec turned back on by hand, requires a deliberate override. A token read by another local process on a shared machine is a different matter, because there the server is still bound to localhost and the defaults stay true.
Root cause
From the advisory and the fix pull request, in plain terms:
- Vite applies its default deny list only if
server.fs.denyis unset after config hooks run. - Vite compiles the list into a matcher during config resolution, so changing the array later does nothing.
- On project servers, Vitest set
denyto just the token file inside aconfig()hook. Setting it replaced Vite's defaults, so.env, keys,.npmrcand.gitwere no longer denied. - On the root server, Vitest pushed the token entry onto the array after Vite had already compiled the matcher, so the token entry never took effect.
The same missing defaults on project servers also loosened Browser Mode's own checks for server.readFile, writeFile, removeFile, screenshots and traces, per the advisory.
The fix, by commit
| Item | Value | Source |
|---|---|---|
| Pull request | vitest-dev/vitest #11578, "fix: keep vite default fs.deny with api token file", branch fix/fs-deny-defaults, 8 commits | PR page |
| Author / merger | Hiroshi Ogawa (hi-ogawa) authored; sheremet-va approved and merged on 14 Oct | PR page |
| Merge commit | 28dc1edfb6ba5a181fe208576989063795aeadb3 | Commit page, NVD references |
| Commit trailers | Co-authored-by Hiroshi Ogawa and Claude Opus 5.5 | Commit page |
| Files changed | packages/vitest/src/node/config/resolveConfig.ts, packages/vitest/src/node/plugins/index.ts, packages/vitest/src/node/plugins/utils.ts (23 lines added), packages/vitest/src/node/plugins/workspace.ts, and a new test/e2e/test/fs-deny.test.ts (93 lines); 121 lines added and 6 removed in total | Commit page |
| Release | v5.0.5, release commit 7a825db, published to npm 15 Oct 14:29:01 UTC | Release page, npm registry |
The change adds a helper, appendApiTokenFsDeny, called from the config hooks for both the root and project servers. If you set fs.deny yourself, it keeps your values and appends the token file. If you did not, it starts from a copy of Vite's default list and appends the token file. The late push in resolveConfig.ts and the replacing deny in workspace.ts are removed. A new end-to-end test checks the root server, an inline project sharing it, and a project with its own server.
The pull request notes a design limit: Vite does not export its default list, so Vitest now carries its own copy of the newest Vite defaults. On older Vite versions that copy is stricter than Vite's own. The author said they planned to ask the Vite project to export the defaults.
Indicators of compromise
None published. The advisory includes no indicators, and CISA's SSVC entry on NVD records exploitation as "none". There is no file hash or network indicator to hunt for. If you want evidence, the most useful local artifacts are shell history and CI logs showing --ui, --api or Browser Mode runs on affected versions, and any port publishing or forwarding configuration for ports 51204 and 63315 (the documented defaults for the API server and Browser Mode).
Related Vitest advisories in 2026
This is not the first Vitest dev server advisory this year. Titles and severities as listed on the Vitest security advisories page:
| Published (2026) | GHSA | Title (abridged) | Severity |
|---|---|---|---|
| 16 Oct | GHSA-xp26-3454-mjgp | Windows backslash traversal bypasses the @vitest/mocker redirect allowlist fix | Moderate |
| 16 Oct | GHSA-qxg6-89mg-rqv8 | Code injection via unauthenticated manual mock registration | Moderate |
| 16 Oct | GHSA-p458-7748-mcp3 | This advisory | Moderate |
| 18 Aug | GHSA-82fw-gwwq-j7x9 | Path traversal / arbitrary file read via @vitest/mocker redirect mock | Moderate |
| 8 Jul | GHSA-p63j-vcc4-9vmv | Browser Mode provider commands bypass the file-access permission gate | Critical |
| 1 Jun | GHSA-g8mr-85jm-7xhm | Exposed Browser Mode API can proxy CDP and overwrite config files, leading to RCE | Critical |
| 19 May | GHSA-2h32-95rg-cppp | Browser mode serves unsanitized otelCarrier query parameter as inline script | Critical |
| 19 May | GHSA-5xrq-8626-4rwp | Arbitrary file can be read and executed when Vitest UI server is listening | Critical |
The pattern is the point. Test tooling now runs real HTTP servers with privileged APIs on developer machines. The project has been hardening that surface all year, and the token file at the center of this CVE was itself part of that hardening. Upgrading once is not enough; treat the test runner like any other server dependency and keep it current.
How it works, in plain language
Vite keeps a "never serve" list: environment files, keys, registry config, the git directory. Vitest reuses Vite to serve its UI and the pages that run tests in a real browser. In Vitest 5 the maintainers added a secret token for the UI and put the token file on that list. On one kind of server, Vitest set a list containing only the token, which quietly threw away Vite's defaults. On the other, it added the token after Vite had locked the list, so the addition did nothing. The fix builds the full list (Vite's defaults plus the token) before Vite locks it, on every server. Nothing in your built or deployed application changes.
Who is affected
| Situation | Affected? | Why |
|---|---|---|
Vitest 5.0.0-beta.6 to 5.0.4, Browser Mode with test.projects, developer browsing the web while tests run | Yes, highest practical risk | Project servers serve .env and keys; a web page can read JavaScript-compatible .env lines through the browser |
Vitest 5 affected versions, --ui or api on, server bound to localhost, writable home directory | Lower | Default deny list holds on the root server; token sits outside the served paths |
Same, but in a container or runner without a writable HOME | Yes, token exposure | Token falls back into node_modules/.vitest/ and the root server serves it |
Any affected setup where the port is reachable from other machines (api.host, published container port, port forwarding, cloud workspace port sharing) | Yes | Full files readable over plain HTTP by anyone who can reach the port |
| Shared machines or self-hosted runners where other users or jobs can reach localhost | Possibly | Local access is one of the advisory's named routes |
CI running plain vitest run | No | No port is opened |
| Vitest 4.x or earlier | No | Per the advisory |
| Production builds and deployments | No | Per the advisory |
| vite-plus 1.0.0-rc.0 to 1.2.0 | Yes, under the same conditions | Bundles an affected vitest |
What to do now
The order matters. Find every copy first, upgrade, then work out what to rotate based on how each affected copy was used.
1. Check a single repo
Ask each package manager what it actually resolved. Look for vitest, @vitest/ui, @vitest/browser and vite-plus.
# npm
npm ls vitest @vitest/ui @vitest/browser vite-plus --all
# pnpm (workspace-wide)
pnpm why -r vitest
pnpm why -r @vitest/ui @vitest/browser vite-plus
# Yarn (Berry)
yarn why vitest
yarn why @vitest/ui
# Bun
bun pm ls --all | grep -E "vitest|vite-plus"
# The binary you actually run
npx vitest --version
Then read the lockfile directly, which works without installing anything:
# package-lock.json (npm v7+ lockfile format)
jq -r '.packages | to_entries[]
| select(.key|test("node_modules/(vitest|@vitest/(ui|browser)|vite-plus)$"))
| "\(.key) \(.value.version)"' package-lock.json
# pnpm-lock.yaml
grep -nE "^\s+'?(vitest|@vitest/(ui|browser)|vite-plus)@" pnpm-lock.yaml
# yarn.lock
grep -nE '^"?(vitest|@vitest/(ui|browser)|vite-plus)@' -A2 yarn.lock
Any vitest, @vitest/ui or @vitest/browser version from 5.0.0-beta.6 up to 5.0.4, or vite-plus from 1.0.0-rc.0 to 1.2.0, is affected.
2. Check whether you ran an affected server
Version alone does not tell you if anything leaked. Look at how Vitest was configured and launched.
# Scripts that start a server
grep -nE "vitest[^\"]*(--ui|--api|--browser)" package.json
# Config that enables the UI, API or Browser Mode, and projects
grep -nE "ui:|api:|browser:|projects:|host:" vitest.config.* vite.config.* 2>/dev/null
# Was the token ever written to the fallback location?
ls -la node_modules/.vitest/.vitest-secret-token 2>/dev/null
If node_modules/.vitest/.vitest-secret-token exists, Vitest could not write to the user data directory on that machine at some point, and on affected versions the root server would have served that file. Note the path and the machine.
Also check for any of these, which turn "localhost only" into "reachable":
hostset underapi(or the deprecatedbrowser.api) to0.0.0.0or a LAN address, or--api.hoston the command line- Docker
-p 51204:51204or-p 63315:63315, or the same ports underports:indocker-compose.yml forwardPortsin.devcontainer/devcontainer.jsonssh -Lor-Rtunnels, and any cloud workspace port sharing set to public or organization
3. Check every repo in an organization
A local loop over cloned repos catches lockfiles in monorepo sub packages too:
# Run from a directory that holds your cloned repos
find . -name node_modules -prune -o \
\( -name package-lock.json -o -name pnpm-lock.yaml -o -name yarn.lock -o -name bun.lock \) -print |
while read -r lf; do
hits=$(grep -hoE "(vitest|@vitest/(ui|browser))@?[\"': ]*5\.0\.(0-(beta\.[67]|rc\.[1-4])|[0-4])\b|vite-plus@?[\"': ]*1\.(0\.0-rc\.0|0\.0|1\.0|2\.0)\b" "$lf" | sort -u)
[ -n "$hits" ] && echo "$lf: $hits"
done
The pattern is deliberately loose; confirm each hit with the package manager command from step 1. Lockfile formats differ, so treat this as a triage list, not a verdict.
If your code lives on GitHub, code search can find manifests that pin a 5.0 range, though it will not see what a caret range resolved to:
gh search code --owner YOUR_ORG '"vitest": "^5' --filename package.json
gh search code --owner YOUR_ORG '"@vitest/browser"' --filename package.json
One tooling caveat. The reviewed GitHub Advisory Database entry lists @vitest/ui, @vitest/browser and vite-plus as the affected packages; vitest appears in the repository advisory and NVD but not as its own row in the reviewed entry at the time of our fetch. A tool that matches only on that entry's package list may say nothing for a repo that depends on vitest alone and turns on api. Do not treat a quiet audit as a pass; check the vitest version directly.
4. Check CI images and dev container images
Images often bake in node_modules, and some bake in .env files too. Check both.
IMG=your-registry/ci-node:tag
# Find every vitest and vite-plus copy in the image and print its version
docker run --rm --entrypoint sh "$IMG" -c '
find / -path /proc -prune -o \
\( -path "*/node_modules/vitest/package.json" \
-o -path "*/node_modules/@vitest/ui/package.json" \
-o -path "*/node_modules/@vitest/browser/package.json" \
-o -path "*/node_modules/vite-plus/package.json" \) -print 2>/dev/null |
while read -r p; do printf "%s " "$p"; grep -m1 "\"version\"" "$p"; done'
# Look for a fallback token file or secret files baked into the image
docker run --rm --entrypoint sh "$IMG" -c '
find / -path /proc -prune -o \
\( -name .vitest-secret-token -o -name ".env" -o -name ".env.*" -o -name .npmrc \) -print 2>/dev/null'
# Is HOME writable for the user the image runs as?
docker run --rm --entrypoint sh "$IMG" -c 'echo "user=$(id -u) HOME=$HOME"; touch "$HOME/.probe" && echo writable || echo NOT writable'
A HOME that is not writable is the condition that pushes the token into node_modules. Fixing it is worth doing regardless of this CVE.
In CI configuration, look for jobs that run vitest --ui, vitest --browser or set api, and for runners where jobs share a host. GitHub Actions example:
grep -rnE "vitest.*(--ui|--browser|--api)|services:|ports:" .github/workflows/
5. Upgrade
Keep every @vitest/* package on the same version as vitest. The advisory notes that @vitest/ui and @vitest/browser pin vitest as a peer dependency, so a half upgrade will fail peer checks or leave the old core in place.
# npm
npm install -D vitest@5.0.5 @vitest/ui@5.0.5 @vitest/browser@5.0.5
# pnpm (all workspace packages)
pnpm up -r -D vitest@5.0.5 @vitest/ui@5.0.5 @vitest/browser@5.0.5
# Yarn (Berry)
yarn up vitest@5.0.5 @vitest/ui@5.0.5 @vitest/browser@5.0.5
# Bun
bun add -d vitest@5.0.5 @vitest/ui@5.0.5 @vitest/browser@5.0.5
# vite-plus users
npm install -D vite-plus@1.3.0
Install only the @vitest/* packages you already use. If you use a Browser Mode provider package from the @vitest scope, move it to the same version as well.
If an affected vitest arrives transitively (for example through an internal toolkit package), force it while you wait for the owner to publish:
{
"overrides": { "vitest": "5.0.5" },
"pnpm": { "overrides": { "vitest": "5.0.5" } },
"resolutions": { "vitest": "5.0.5" }
}
Use only the block your package manager reads: overrides for npm, pnpm.overrides for pnpm, resolutions for Yarn.
Then verify and rebuild:
npx vitest --version # expect 5.0.5 (or later)
npm ls vitest --all # no 5.0.0-5.0.4 left anywhere
Rebuild any CI or dev container image that bakes node_modules, and bump the image tag your pipelines reference. An updated lockfile does nothing for an image built last week.
6. If you cannot upgrade today
The advisory's workaround is to set server.fs.deny explicitly in the root Vitest config, using Vite's default list for your Vite version plus the token file. With an explicit list, Vite never drops its defaults, because you supplied them. For Vite 8.3, matching the advisory's example:
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
server: {
fs: {
deny: [
'.env',
'.env.*',
'*.{crt,pem,key,p12,pfx,cer,der}',
'.npmrc',
'.yarnrc.yml',
'**/.git/**',
'.vitest-secret-token',
],
},
},
})
Check your own Vite version's documented defaults before copying. In the meantime, avoid running Browser Mode with test.projects while browsing unrelated sites, keep every server bound to localhost, and make HOME writable in containers so the token stays out of the workspace.
7. Decide what to rotate
Rotation should follow exposure, not panic. Use this order:
- Did any affected copy ever run a server? If every affected copy only ran
vitestorvitest run, the advisory says nothing was reachable. Upgrade and stop. - Was the port reachable from anything other than the developer's own browser? If the server was bound to a non-localhost host, published from a container, forwarded, shared from a cloud workspace, or ran on a host shared with other users or jobs, treat every file in the exposure table under the workspace as potentially read. Rotate all of it.
- Browser Mode with
test.projects, localhost only? The browser route leaks only.envlines that are valid JavaScript, and stops at the first line that is not. Read the.envfiles that sat under the project root on those machines. Rotate every value that would have parsed (quoted strings, numbers, and similar) above the first line that would not. If you are unsure, rotate the whole file. Also consider whether another local dev server on the same machine could have served hostile content, since the advisory notes localhost origins are allowed by Vite's CORS default. - Token in the fallback location? If
node_modules/.vitest/.vitest-secret-tokenexisted on a reachable server, assume the token was read. Delete the file and check that a new one is created on the next run. PR #10583 describes the token as generated once and reused, so it will not change unless you remove it. Then review recent changes to test files on that machine, since a token holder could write or rerun tests whereapi.allowWriteorapi.allowExecwere enabled.
What to rotate, by file:
| File | Rotate |
|---|---|
.env, .env.* | Every API key, database password and cloud credential in the file. Revoke, then reissue; do not just add a new key next to the old one. |
.npmrc, .yarnrc.yml | npm and private registry tokens. Revoke in the registry, issue a new token with the narrowest scope, prefer read-only tokens on developer machines. |
Keys and certificates (.pem, .key, .p12, .pfx, .crt, .cer, .der) | Private keys: generate new key pairs and reissue certificates. Public certificates on their own are not secrets, but a .p12 or .pfx usually bundles the private key. |
.git | Check .git/config for credentials embedded in remote URLs and rotate those tokens. History itself is not a credential, but if a secret was ever committed and later removed, it is still in history; rotate it. |
.vitest-secret-token | Delete and let Vitest generate a new one. |
After rotation, review provider logs for the old credentials for unusual use since the affected version was first installed on that machine. That date is in your lockfile history: git log -p -- package-lock.json | grep -n "vitest" shows when the version changed.
Safe dev server configuration for Vitest
The fix closes this bug. The settings below shrink the next one.
| Setting | Recommended | Why |
|---|---|---|
api.host (and any host flag) | Leave unset, which binds to localhost per Vitest's docs | The advisory's network route needs a developer-set exposure |
api.allowWrite, api.allowExec | Leave at defaults; never set true on an exposed server | Vitest's docs warn that an exposed server with these on lets anyone who connects run code |
server.fs.deny | Optional explicit list that includes Vite's defaults and your own sensitive paths | Belt and braces if a future change drops the defaults again |
server.fs.allow | Keep to the workspace root; do not widen to home directories | Limits what any deny bypass could reach |
Container HOME | Writable for the runtime user | Keeps the token out of node_modules |
| Container ports | Do not publish 51204 or 63315; if you must, bind to 127.0.0.1 (for example -p 127.0.0.1:51204:51204) | Publishing on all interfaces is the most common accidental exposure |
Dev container forwardPorts | Forward only what you need, keep port visibility private | Same reason |
.env placement | Keep real secrets out of the repository tree; inject them at run time from a secret manager or the CI provider's secret store | A file that is not under the served root cannot be served |
.dockerignore | Exclude .env*, *.pem, *.key, .npmrc, .git | Stops secrets being baked into images |
| Registry tokens on laptops | Read-only, scoped, short lived | Limits the value of a leaked .npmrc |
| Browser habits | Treat a Chrome Local Network Access prompt from an unfamiliar site as a reason to say no | The advisory says recent Chrome versions may ask before a page can reach the local server |
| Test secrets | Use fake or test-only credentials for test runs | Tests rarely need production keys |
Where sources differ
- Which packages are affected. The reviewed GitHub Advisory Database entry and OSV list
@vitest/ui,@vitest/browserandvite-plus. The repository advisory and NVD also listvitest, and every source says the fix ships invitest5.0.5. We treatvitestas affected. - Publication date. The repository advisory says 16 October; NVD published 20 October; the reviewed global entry is 21 October; our lead said 22 October, which is when it reached Hacker News.
- Credits. The repository advisory credits sondt99 (reporter) and tomohiro86 (finder). The global database page also lists tikket1.
- Release timestamp. The GitHub release page shows 15 October 14:22 without a timezone; the npm registry records the 5.0.5 publish at 14:29:01 UTC.
- Severity. Only GitHub has scored it (6.5). NVD has not done its own analysis yet.
browser.api.host. The advisory mentions it as an exposure route; Vitest's v5 migration guide saysbrowser.apiis deprecated and has no effect, with settings moving to the top-levelapi. Older configs carried forward may still contain it, so check for both.
Open questions
- Whether the reviewed GitHub Advisory Database entry will add
vitestas its own affected package. Until it does, alerting onvitestalone may be inconsistent across tools. - When NVD will complete analysis and whether its score will match GitHub's 6.5.
- Whether Vite will export its default deny list so frameworks can extend it safely, as the fix's author proposed. Until then, Vitest carries a copy that has to be kept in sync by hand.
- No exploitation has been reported, and CISA's SSVC entry says "none". Because the main risk is quiet reading of local files, absence of reports is weak evidence either way. Your own exposure assessment matters more than public telemetry here.
What would have helped
Practices any team can use
- Know where your test tooling runs, and how. An inventory that records not just "repo X uses vitest 5.0.3" but "repo X runs Browser Mode with projects in its dev container" turns a two-day scramble into a one-hour query. Keep the test runner in the same dependency inventory as runtime packages.
- Upgrade on patch releases, not only on alerts. The fix was on npm six days before the reviewed advisory. Automated update pull requests for dev dependencies, merged on a schedule, would have closed the gap before most teams knew it existed.
- Treat dev ports as production ports. No
0.0.0.0for tooling servers, no publishing tooling ports from containers, private port visibility in cloud workspaces. Most of the serious routes in this advisory require one of these. - Keep secrets out of the working tree. Inject secrets at run time from a secret manager or the CI secret store. Use test-only credentials for test runs. A file that does not exist under the served root cannot be served by any bug.
- Give containers a writable home. The token fallback only triggers when
HOMEis not writable. A writable, per-user home directory in CI and dev images keeps tool state out ofnode_modules. - Scope registry tokens. Read-only, short lived, per machine. A leaked
.npmrcthen costs a revocation, not a supply chain incident. Our post on the Tensorlake npm compromise covers why publish tokens deserve special care. - Rehearse rotation. Know, for each secret type, who can revoke it and how long it takes. The compromised dependency playbook has a template that works for this case too.
Where Safeguard fits
Safeguard helps with the find and fix half of this incident. It does not help with deciding which secrets were read, and it would not have prevented the bug.
- Inventory across repos and images. Safeguard's SBOM and dependency scanning covers repositories, container images and SBOM files, with full transitive npm resolution. From the CLI:
safeguard scan --dir .for a repo,safeguard scan --image your-registry/ci-node:tagfor a CI or dev container image, andsafeguard scan --sbom sbom.cdx.jsonfor an SBOM you already produce. Results are written as JSON under./output. In the platform you can see which projects contain a given component and version, which answers "where is vitest 5.0.0 to 5.0.4" across everything you have connected. - Matching against vulnerability databases. Findings come from matching components against vulnerability databases, so the result depends on the advisory data those databases hold. Given the package listing gap described above, check that
vitestitself is flagged, and if not, query by component and version. - Fix pull requests. Autonomous remediation can open fix PRs that bump
vitestand the matching@vitest/*packages together. It is off by default; you turn it on. - Policies and gates. A gate evaluated in the platform can block merges or image promotions that still carry an affected version.
- The assistant, with your approval. The Safeguard CLI's assistant can edit
package.json, add the explicitserver.fs.denyworkaround, run the install and the test suite, and rebuild an image, asking before each command in its default permission mode./permissionsswitches between Ask, Accept edits, Plan, Auto (default) and Strict. In CI,safeguard execruns the same work non-interactively.
Where it does not help: Safeguard does not tell you which .env values a dev server served, whether a token was read, or which credentials to revoke. It does not inspect developer browsers, port forwarding or cloud workspace sharing settings. Rotation and log review stay with your team and your providers.
Zero-Day Discovery and Gold
- Zero-Day Discovery looks for previously unknown flaws before a CVE exists, in an agentic mode and a deterministic scanner mode, and issues findings as SGZ advisories before they reach public CVE databases. It covers seven package ecosystems. We make no claim that it found this flaw or that an SGZ advisory existed for it; this bug was found and reported to the Vitest project by outside researchers.
- Gold Open Source at gold.safeguard.sh is free with no login: searchable CVE intelligence with CVSS, EPSS, KEV status, affected and fixed versions and a recommended safe version, a free read API, and a CI gate GitHub Action.
- Gold Registry at registry.safeguard.sh is the paid tier: hardened drop-in components and container images held to zero exploitable critical or high CVEs, SLSA L3 or higher, Sigstore signed, with evidence pages; an artifact loses its badge if it regresses.
The habit these support is the one this incident rewards: pull patched, signed components and base images from a curated source, and rebuild and redeploy tooling images as quickly as runtime images when a fix lands.
Safeguard's desktop and CLI apps are at https://safeguard.sh/download, and CLI docs are at https://docs.safeguard.sh/docs/cli .
Common mistakes
- Upgrading
@vitest/uiwithoutvitest. The peer pin means the core is where the fix lives. Upgrade them together. - Trusting a clean audit. If your tool reads only the reviewed advisory's package list, a repo with
vitestalone andapienabled may not be flagged. Check versions directly. - Updating the lockfile but not the image. CI and dev container images that bake
node_moduleskeep the old version until rebuilt and re-tagged. - Rotating everything everywhere. Machines that only ran
vitest runhad no open port. Spend rotation effort where a server actually ran and could be reached. - Rotating nothing because "it is only localhost". Published container ports, forwarded ports, shared runners and Browser Mode with projects all break that assumption.
- Copying an old deny list. The workaround must match your Vite version's defaults plus the token file. A shorter list is worse than none.
- Leaving
node_modules/.vitest/.vitest-secret-tokenin place after upgrading. The upgrade stops the server serving it, but if it was exposed before, delete it so a fresh token is issued.
FAQ
Is CVE-2026-112948 being exploited?
No exploitation has been reported. CISA's SSVC entry on NVD records exploitation as "none" and automatable as "no". The advisory publishes no indicators of compromise. Because the risk is quiet reading of local files, assess your own exposure rather than relying on the absence of reports.
Does this affect my production app?
No. The advisory says applications built or deployed from the project are not affected. Only the machine running Vitest is exposed, and only while it runs the UI, Browser Mode or the API server.
I only run vitest run in CI. Do I need to act?
Upgrade anyway, but you have nothing to rotate on account of those jobs. Plain vitest and vitest run open no port. Check that no job uses --ui, --browser or api.
Which exact files could leak?
.env and .env.*, files ending in .crt, .pem, .key, .p12, .pfx, .cer or .der, .npmrc, .yarnrc.yml, anything under .git, and .vitest-secret-token, within the paths the server can reach. Which of these leaked depends on which server you ran, as in the table above.
What versions are fixed?
vitest, @vitest/ui and @vitest/browser 5.0.5, and vite-plus 1.3.0. Vitest 4.x and earlier were never affected. Later releases, such as 5.1.0-beta.1 published 19 October, came after the fix.
Why does the GitHub Advisory Database not list vitest itself?
At the time of our fetch, the reviewed entry listed @vitest/ui, @vitest/browser and vite-plus, while its text says the fix ships in vitest. The repository advisory and NVD both include vitest. We do not know why the reviewed entry differs. Treat vitest 5.0.0-beta.6 to 5.0.4 as affected.
Why is this rated only Moderate if .env files can leak?
The CVSS vector requires user interaction and covers confidentiality only. The browser route reads only .env lines that are valid JavaScript and stops at the first line that is not; the full file route needs local access or a deliberate network exposure. For a team whose dev containers publish ports, the real world risk can be higher than 6.5 suggests.
What is the Vitest API token, and why does it matter?
Vitest 5 added token authentication for the UI, the attachment endpoint and the API WebSocket. Per the advisory, that RPC can write and rerun test files when api.allowWrite or api.allowExec is enabled. A leaked token is an access credential to your test runner, so delete it if it was exposed.
Can I just set server.fs.deny instead of upgrading?
That is the advisory's workaround, and it works if the list matches your Vite version's defaults plus .vitest-secret-token. It is a stopgap. The upgrade is the fix, and it also keeps you current for the other Vitest advisories published the same day.
Does Safeguard tell me what an attacker could have read?
No. Safeguard helps you find affected vitest versions across repos and images, open fix PRs and gate releases. Deciding what was read and rotating credentials is work for your team and your credential providers.
Fix it without waiting for upstream
In incidents like this one, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would have helped
These demos show general Safeguard flows, not this incident. Each intro says how the same flow applies here.
The CLI demo fixes a lodash CVE: it reads the finding, asks before each command, edits package.json, runs npm install and npm test with your approval and pushes nothing. For CVE-2026-112948 you would ask the same assistant to move vitest, @vitest/ui and @vitest/browser to 5.0.5 together, rerun the suite, and add the explicit server.fs.deny list if you cannot upgrade yet.
Click through it at your own pace, or open it full screen.
The local runner demo sets up the runner on your own machine, approves it with a device code, then scans from the terminal. Here you would point the same scan at your CI and dev container images, which is where baked node_modules with an old vitest tend to hide.
Click through it at your own pace, or open it full screen.
The desktop app demo shows a real session: you ask for work on your computer, pick the folder, then Approve or Deny each command and file edit, ending with the upgrade done and tests passing. For this CVE that folder would be a repo with Browser Mode projects, and each approval is a chance to check the change touches only the test tooling.
Click through it at your own pace, or open it full screen.
For a wider look at this month's incidents, see October 2026 security incidents and what would have helped, and for more on AI-assisted vulnerability reports like this one, see AI-found vulnerabilities.
Quick check
Which Vitest setup lets a web page read .env values through the developer's browser?
Browser Mode with test.projects on vitest 5.0.0-beta.6 to 5.0.4. Each project server drops Vite's default deny list and serves .env as JavaScript, so lines with valid JavaScript values can be read by a page loading it as a script.
When is the Vitest API token file served, and where is it?
On the root server (UI, API or single project Browser Mode) of an affected version, only when Vitest falls back to node_modules/.vitest/.vitest-secret-token because the user data directory is not writable, for example in a container without a writable HOME.
Sources
- GitHub Advisory Database: GHSA-p458-7748-mcp3 (CVE-2026-112948)
- Vitest repository security advisory GHSA-p458-7748-mcp3
- Vitest security advisories list
- Vitest v5.0.5 release notes
- Vitest pull request #11578, keep vite default fs.deny with api token file
- Vitest fix commit 28dc1ed
- Vitest pull request #10583, harden UI API access
- NVD API record for CVE-2026-112948
- OSV entry for GHSA-p458-7748-mcp3
- npm registry metadata for vitest
- npm registry metadata for vite-plus
- vite-plus v1.3.0 release notes
- Vite server options documentation
- Vitest api configuration documentation
- Vitest UI guide
- Vitest migration guide
- npm docs: auditing package dependencies
- Hacker News search results for CVE-2026-112948
Last reviewed: 22 October 2026. This is a developing story; check the linked advisories for the latest status.