Safeguard
Supply Chain Security•

Vitest CVE-2026-112948: Dev Server Secret Leak, What to Check and Rotate

CVE-2026-112948 lets Vitest 5 dev servers serve .env, keys, .npmrc, .git and the API token. Upgrade to vitest 5.0.5 or vite-plus 1.3.0, then rotate.

Safeguard Security Team
41 min read

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)EventSource
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:08vitest@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:30vitest@5.0.0 stable is published.npm registry
22 Sep, 13:57:15vite-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:02vitest@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:01vitest@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:24vitest@4.1.12 is published on the 4.x line, which the advisory says is not affected.npm registry
16 Oct, 05:42:37vite-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:05NVD publishes CVE-2026-112948 with status "Awaiting Analysis". A CISA ADP SSVC entry records exploitation as "none".NVD API
21 Oct, 19:57:00The reviewed entry appears in the GitHub Advisory Database (OSV mirror timestamp).OSV.dev
22 Oct, 09:57:57The 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

FieldValueSource
CVECVE-2026-112948GitHub advisory, NVD
GHSAGHSA-p458-7748-mcp3GitHub 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.5GitHub advisory; NVD lists it as "MEDIUM" from security-advisories@github.com
CVSS 3.1 vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:NGitHub advisory, NVD, OSV
NVD's own scoreNone yet; status "Awaiting Analysis"NVD API, last modified 20 Oct 19:56:45
CISA ADP SSVCExploitation: none; Automatable: no; Technical impact: partialNVD API
EPSS0.406% (34th percentile) at the time of our fetchGitHub Advisory Database page
CWECWE-200 (Exposure of Sensitive Information to an Unauthorized Actor), CWE-552 (Files or Directories Accessible to External Parties)GitHub advisory, NVD
CreditsReporter: sondt99. Finder: tomohiro86. The global database page also lists tikket1.Repository advisory; GitHub Advisory Database page
Report noteThe 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

PackageAffectedFixedNotesSource
vitest>= 5.0.0-beta.6, < 5.0.55.0.5The 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.55.0.5Pins 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.55.0.5Same peer pin.GitHub advisory, OSV
vite-plus>= 1.0.0-rc.0, < 1.3.01.3.0Bundles vitest; 1.3.0 depends on vitest 5.0.5.GitHub advisory, npm registry
vitest 4.x and earlierNot affectedn/aThe 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:

PatternWhat 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, *.derTLS private keys and certificates for local HTTPS, client certificates, signing keys, service account keys saved as PEM
.npmrcnpm registry auth tokens, including private registry tokens
.yarnrc.ymlYarn Berry registry auth tokens (npmAuthToken)
**/.git/**Repository history and config, which can include credentials embedded in remote URLs
.vitest-secret-tokenThe 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 VitestListens on a port?Vite default deny files.vitest-secret-token
Plain vitest or vitest runNoNot reachableNot reachable
--ui, or api enabled (root server)YesDeniedServed, only in the fallback location
Browser Mode, single project (root server)YesDeniedServed, only in the fallback location
Browser Mode with test.projects (each project's server)YesServedDenied

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:

  1. A web page in the developer's browser, only in Browser Mode with test.projects. Because the project server returns a .env file 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, an export prefix) stops the script entirely; unquoted values stop it at that line, so only earlier lines leak; cross-origin fetch and XHR reads are blocked by Vite's default CORS policy; the Host check 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.
  2. 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.deny is 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 deny to just the token file inside a config() hook. Setting it replaced Vite's defaults, so .env, keys, .npmrc and .git were 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

ItemValueSource
Pull requestvitest-dev/vitest #11578, "fix: keep vite default fs.deny with api token file", branch fix/fs-deny-defaults, 8 commitsPR page
Author / mergerHiroshi Ogawa (hi-ogawa) authored; sheremet-va approved and merged on 14 OctPR page
Merge commit28dc1edfb6ba5a181fe208576989063795aeadb3Commit page, NVD references
Commit trailersCo-authored-by Hiroshi Ogawa and Claude Opus 5.5Commit page
Files changedpackages/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 totalCommit page
Releasev5.0.5, release commit 7a825db, published to npm 15 Oct 14:29:01 UTCRelease 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)GHSATitle (abridged)Severity
16 OctGHSA-xp26-3454-mjgpWindows backslash traversal bypasses the @vitest/mocker redirect allowlist fixModerate
16 OctGHSA-qxg6-89mg-rqv8Code injection via unauthenticated manual mock registrationModerate
16 OctGHSA-p458-7748-mcp3This advisoryModerate
18 AugGHSA-82fw-gwwq-j7x9Path traversal / arbitrary file read via @vitest/mocker redirect mockModerate
8 JulGHSA-p63j-vcc4-9vmvBrowser Mode provider commands bypass the file-access permission gateCritical
1 JunGHSA-g8mr-85jm-7xhmExposed Browser Mode API can proxy CDP and overwrite config files, leading to RCECritical
19 MayGHSA-2h32-95rg-cpppBrowser mode serves unsanitized otelCarrier query parameter as inline scriptCritical
19 MayGHSA-5xrq-8626-4rwpArbitrary file can be read and executed when Vitest UI server is listeningCritical

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

SituationAffected?Why
Vitest 5.0.0-beta.6 to 5.0.4, Browser Mode with test.projects, developer browsing the web while tests runYes, highest practical riskProject 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 directoryLowerDefault deny list holds on the root server; token sits outside the served paths
Same, but in a container or runner without a writable HOMEYes, token exposureToken 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)YesFull 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 localhostPossiblyLocal access is one of the advisory's named routes
CI running plain vitest runNoNo port is opened
Vitest 4.x or earlierNoPer the advisory
Production builds and deploymentsNoPer the advisory
vite-plus 1.0.0-rc.0 to 1.2.0Yes, under the same conditionsBundles 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":

  • host set under api (or the deprecated browser.api) to 0.0.0.0 or a LAN address, or --api.host on the command line
  • Docker -p 51204:51204 or -p 63315:63315, or the same ports under ports: in docker-compose.yml
  • forwardPorts in .devcontainer/devcontainer.json
  • ssh -L or -R tunnels, 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:

  1. Did any affected copy ever run a server? If every affected copy only ran vitest or vitest run, the advisory says nothing was reachable. Upgrade and stop.
  2. 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.
  3. Browser Mode with test.projects, localhost only? The browser route leaks only .env lines that are valid JavaScript, and stops at the first line that is not. Read the .env files 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.
  4. Token in the fallback location? If node_modules/.vitest/.vitest-secret-token existed 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 where api.allowWrite or api.allowExec were enabled.

What to rotate, by file:

FileRotate
.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.ymlnpm 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.
.gitCheck .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-tokenDelete 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.

SettingRecommendedWhy
api.host (and any host flag)Leave unset, which binds to localhost per Vitest's docsThe advisory's network route needs a developer-set exposure
api.allowWrite, api.allowExecLeave at defaults; never set true on an exposed serverVitest's docs warn that an exposed server with these on lets anyone who connects run code
server.fs.denyOptional explicit list that includes Vite's defaults and your own sensitive pathsBelt and braces if a future change drops the defaults again
server.fs.allowKeep to the workspace root; do not widen to home directoriesLimits what any deny bypass could reach
Container HOMEWritable for the runtime userKeeps the token out of node_modules
Container portsDo 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 forwardPortsForward only what you need, keep port visibility privateSame reason
.env placementKeep real secrets out of the repository tree; inject them at run time from a secret manager or the CI provider's secret storeA file that is not under the served root cannot be served
.dockerignoreExclude .env*, *.pem, *.key, .npmrc, .gitStops secrets being baked into images
Registry tokens on laptopsRead-only, scoped, short livedLimits the value of a leaked .npmrc
Browser habitsTreat a Chrome Local Network Access prompt from an unfamiliar site as a reason to say noThe advisory says recent Chrome versions may ask before a page can reach the local server
Test secretsUse fake or test-only credentials for test runsTests rarely need production keys

Where sources differ

  • Which packages are affected. The reviewed GitHub Advisory Database entry and OSV list @vitest/ui, @vitest/browser and vite-plus. The repository advisory and NVD also list vitest, and every source says the fix ships in vitest 5.0.5. We treat vitest as 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 says browser.api is deprecated and has no effect, with settings moving to the top-level api. Older configs carried forward may still contain it, so check for both.

Open questions

  • Whether the reviewed GitHub Advisory Database entry will add vitest as its own affected package. Until it does, alerting on vitest alone 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.0 for 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 HOME is not writable. A writable, per-user home directory in CI and dev images keeps tool state out of node_modules.
  • Scope registry tokens. Read-only, short lived, per machine. A leaked .npmrc then 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:tag for a CI or dev container image, and safeguard scan --sbom sbom.cdx.json for 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 vitest itself is flagged, and if not, query by component and version.
  • Fix pull requests. Autonomous remediation can open fix PRs that bump vitest and 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 explicit server.fs.deny workaround, run the install and the test suite, and rebuild an image, asking before each command in its default permission mode. /permissions switches between Ask, Accept edits, Plan, Auto (default) and Strict. In CI, safeguard exec runs 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/ui without vitest. 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 vitest alone and api enabled may not be flagged. Check versions directly.
  • Updating the lockfile but not the image. CI and dev container images that bake node_modules keep the old version until rebuilt and re-tagged.
  • Rotating everything everywhere. Machines that only ran vitest run had 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-token in 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 situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingThe Gold scan GitHub Action in CI, plus Safeguard policies and gates
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch (early access)
You want to start clean on new projectsPull 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

Last reviewed: 22 October 2026. This is a developing story; check the linked advisories for the latest status.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.