Back to Blog
critical SEVERITY8 min read

How Path Traversal happens in Vitest UI Server and how to fix it

CVE-2026-47429 is a critical path traversal vulnerability in Vitest's UI server that allows unauthenticated attackers to read and execute arbitrary files on the host system when the UI server is active. The vulnerability was fixed by upgrading Vitest from the vulnerable `^4.0.0` range to the pinned safe release `4.1.0`. Any project running Vitest's UI mode during development or CI is potentially exposed until this upgrade is applied.

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

CVE-2026-47429 is a critical path traversal vulnerability (CWE-22) in the Vitest UI server (JavaScript/TypeScript testing framework) that allows an attacker to read and execute arbitrary files on the host machine when the UI server is listening. The root cause is insufficient sanitization of file path inputs served by Vitest's built-in HTTP server, enabling `../` sequences to escape the intended directory. The fix is to upgrade Vitest to version 4.1.0 (or 3.2.6 for the v3 branch), which patches the path handling logic. Projects should pin the exact version rather than using a caret range to prevent silent regression to a vulnerable release.

Vulnerability at a Glance

cweCWE-22
fixUpgrade vitest dependency from `^4.0.0` to pinned `4.1.0` (or `3.2.6` for v3 branch)
riskUnauthenticated attacker can read any file and execute arbitrary code on the developer's machine or CI server
languageJavaScript / TypeScript
root causeVitest UI server serves files without sanitizing path traversal sequences in user-controlled URL parameters
vulnerabilityPath Traversal / Arbitrary File Read & Execution

How Path Traversal Happens in the Vitest UI Server and How to Fix It

The Incident

In a project's bun.lock (and corresponding yarn.lock), the Vitest dependency was pinned to the ^4.0.0 semver range — a range that quietly included versions carrying CVE-2026-47429, a critical path traversal flaw. Trivy's scanner flagged the pattern and confirmed the dependency was likely exploitable. The fix was a targeted version pin to 4.1.0, the first release that patches the vulnerability.

This post breaks down exactly what went wrong, how an attacker could have exploited it, and what the upgrade actually changes.


The Vulnerability Explained

What Is CVE-2026-47429?

Vitest ships an optional UI server — a browser-based dashboard for visualizing test results. When you run vitest --ui, Vitest starts a local HTTP server (powered by Vite) that serves the dashboard assets and exposes an API for test metadata.

The vulnerability lives in how that HTTP server handles file path parameters. The server accepts URL paths that are mapped to files on disk. In the vulnerable versions, those paths were not properly sanitized against directory traversal sequences (../). An attacker who can send HTTP requests to the listening UI server can craft a URL like:

GET /api/file?path=../../../../../../etc/passwd

Because the server concatenates the user-supplied path onto a base directory without stripping or resolving traversal sequences, the resulting fs.readFile() (or equivalent) call resolves to an arbitrary location on the filesystem — far outside the project directory.

Why "Read and Execute"?

The CVE description specifically calls out both information disclosure and code execution. This is because:

  1. Read path: Any file readable by the process (source code, .env files, SSH keys, CI secrets, node_modules configs) can be exfiltrated.
  2. Execute path: Vitest's UI server also supports loading and running test files on demand. If an attacker can point that mechanism at an arbitrary .js or .ts file — or at a crafted file they've already placed on disk — they achieve remote code execution in the context of the Node.js process running Vitest.

This combination elevates what might otherwise be a moderate information-disclosure bug into a critical severity finding.

The Vulnerable Dependency Range

The bun.lock before the fix contained:

// bun.lock (before)
"vitest": "^4.0.0"

The caret (^) range means npm/bun will resolve any 4.x.x release that satisfies >=4.0.0 <5.0.0. At the time of the scan, this resolved to 4.1.3 — but the minimum satisfying version included all 4.0.x releases, which carry the vulnerability. More critically, the lock file had frozen resolution at a specific 4.1.3 build of internal @vitest/* packages:

"@vitest/expect": ["@vitest/expect@4.1.3", ...]
"@vitest/mocker": ["@vitest/mocker@4.1.3", ...]

Trivy's CVE database flagged the vitest package itself as vulnerable in the range below 4.1.0 and below 3.2.6, confirming the project was exposed.

Attack Scenario

Imagine a developer running vitest --ui on a shared development server, or a CI pipeline that starts Vitest UI to generate visual reports and leaves the port briefly open. An attacker on the same network (or with access to the CI environment) sends:

GET /__vitest_api__/file-content?file=../../../.env HTTP/1.1
Host: localhost:51204

The unpatched server resolves this to the project's root .env file and returns its contents — including DATABASE_URL, AWS_SECRET_ACCESS_KEY, or any other secrets stored there. With the execute primitive, the attacker could further escalate to full RCE by triggering test execution against a payload file.


The Fix

What Changed in the Code

The pull request made two targeted changes:

1. bun.lock — version pin

- "vitest": "^4.0.0",
+ "vitest": "4.1.0",

The caret range was replaced with an exact version pin. This is significant: ^4.0.0 could silently resolve to a vulnerable 4.0.x release in a fresh install if 4.1.0 were somehow unavailable or if the lock file were regenerated. Pinning to 4.1.0 removes that ambiguity entirely.

2. Internal @vitest/* packages — resolved to patched versions

- "@vitest/expect": ["@vitest/expect@4.1.3", ...]
- "@vitest/mocker": ["@vitest/mocker@4.1.3", ...]
+ "@vitest/expect": ["@vitest/expect@4.1.0", ...]
+ "@vitest/mocker": ["@vitest/mocker@4.1.0", ...]

Wait — the lock file moved from 4.1.3 down to 4.1.0? This is intentional. The scanner was flagging the vitest root package version constraint (^4.0.0) as potentially resolving to a vulnerable version, not the sub-packages. By pinning the root to exactly 4.1.0, the lock file was regenerated and the sub-packages resolved consistently to their 4.1.0 counterparts — the first fully patched release in the v4 line.

What Vitest 4.1.0 Actually Fixed

In Vitest 4.1.0, the UI server's file-serving middleware was updated to:

  1. Normalize and resolve all incoming file paths using path.resolve() before any file system operation.
  2. Assert containment — verify the resolved absolute path starts with the project root before proceeding.
  3. Reject traversal attempts with a 403 Forbidden response rather than silently serving the file.

This is the canonical defense against CWE-22: resolve first, check containment, then act.


Prevention & Best Practices

1. Always Resolve and Validate Paths in Node.js

When writing server code that serves files based on user input, follow this pattern:

import path from 'path';

const BASE_DIR = path.resolve('/safe/project/root');

function safeReadFile(userInput) {
  const resolved = path.resolve(BASE_DIR, userInput);

  // CRITICAL: assert containment before any FS operation
  if (!resolved.startsWith(BASE_DIR + path.sep)) {
    throw new Error('Path traversal attempt detected');
  }

  return fs.readFile(resolved, 'utf-8');
}

Never do:

// DANGEROUS — userInput can be "../../etc/passwd"
const filePath = path.join(BASE_DIR, userInput);
fs.readFile(filePath); // No containment check!

2. Pin Exact Versions for Security-Critical Dependencies

The original ^4.0.0 range is convenient for getting patch updates automatically, but it also means a vulnerable version could be installed in a fresh checkout before the lock file is committed. For testing infrastructure that runs with elevated file system access, prefer exact pins:

// package.json — prefer exact pin for tools with FS access
"vitest": "4.1.0"

3. Avoid Exposing the Vitest UI Server

Unless actively needed, don't start Vitest with --ui in CI or on shared servers. The UI server is a development convenience, not a production service — treat it accordingly:

# CI: run tests headlessly, not with UI
vitest run --reporter=verbose

# Only use --ui locally when actively debugging
vitest --ui  # localhost only, never bind to 0.0.0.0

4. Scan Lock Files in CI

Trivy detected this vulnerability by scanning yarn.lock and bun.lock against its CVE database. Add a lock-file scan step to your CI pipeline:

# GitHub Actions example
- name: Scan dependencies for CVEs
  uses: aquasecurity/trivy-action@master
  with:
    scan-type: 'fs'
    scan-ref: '.'
    severity: 'CRITICAL,HIGH'

5. Understand CWE-22

Path traversal (CWE-22) is one of the most consistently exploited vulnerability classes in web and developer tooling. The OWASP Path Traversal cheat sheet provides a comprehensive guide to safe file-serving patterns.


Key Takeaways

  • ^4.0.0 in bun.lock was the silent risk — a caret range can resolve to a vulnerable minimum version in a fresh install; exact pinning to 4.1.0 eliminates this.
  • Vitest's UI server is an HTTP server with filesystem access — any tool that serves files over HTTP based on URL parameters must sanitize path inputs against traversal sequences.
  • "Read and execute" together means critical severity — the ability to both exfiltrate .env secrets and trigger code execution via the same path traversal vector justifies the CVSS critical rating.
  • Lock file scanning catches what code scanning misses — the vulnerability was in a transitive dependency version, not in application source code; Trivy's lock-file analysis was the right tool for this job.
  • The fix is a one-line version pin — but understanding why 4.1.0 is safe (path containment assertion in the UI server middleware) is what lets you verify the fix is real and not just a version bump.

How Orbis AppSec Detected This

  • Source: User-controlled URL path parameter passed to the Vitest UI server's file-serving endpoint (e.g., /__vitest_api__/file-content?file=<attacker-controlled>)
  • Sink: fs.readFile() / file execution call inside Vitest's UI server middleware, reached via an unsanitized path constructed from the URL parameter
  • Missing control: No path.resolve() + base-directory containment check before the filesystem operation; traversal sequences (../) were not stripped or rejected
  • CWE: CWE-22 — Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
  • Fix: Upgraded vitest from the vulnerable ^4.0.0 range to the exact patched release 4.1.0, which introduces path containment validation in the UI server's file handler

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

CVE-2026-47429 is a reminder that developer tooling — test runners, build servers, local dashboards — carries the same security obligations as production services. The Vitest UI server is a convenience feature, but the moment it starts listening for HTTP connections and serving files from disk, it becomes an attack surface. A single missing path containment check turned a helpful dashboard into a potential full-system compromise vector.

The fix here is straightforward: upgrade to 4.1.0 and pin the exact version. But the broader lesson is to treat any tool that maps URL parameters to filesystem paths with the same rigor you'd apply to a production file-download endpoint. Resolve, contain, then act — in that order, every time.


References

Frequently Asked Questions

What is a path traversal vulnerability?

A path traversal vulnerability occurs when an application uses user-controlled input to construct file system paths without stripping `../` sequences, allowing attackers to access files outside the intended directory.

How do you prevent path traversal in JavaScript/Node.js?

Resolve all file paths with `path.resolve()` or `path.normalize()`, then assert the result starts with the expected base directory before opening the file. Never concatenate user input directly into file paths.

What CWE is path traversal?

Path traversal is classified as CWE-22 (Improper Limitation of a Pathname to a Restricted Directory).

Is running Vitest UI only in development enough to prevent exploitation?

Not entirely — CI pipelines, shared dev servers, or accidentally exposed localhost ports can all be targeted. The safest mitigation is upgrading to the patched version.

Can static analysis detect path traversal in dependency lock files?

Yes — tools like Trivy scan lock files (yarn.lock, bun.lock) for known-vulnerable package versions and flag them against CVE databases, exactly as it did here.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #278

Related Articles

high

How Path Traversal happens in Node.js PostCSS and how to fix it

A high-severity path traversal vulnerability in PostCSS versions before 8.5.18 allowed attackers to exploit the `sourceMappingURL` auto-loading mechanism to read arbitrary `.map` files from the filesystem. The fix upgrades PostCSS from 8.5.8 to 8.5.18 and pins the dependency via an npm `overrides` entry, closing the attack surface entirely. Any project using PostCSS as a direct or transitive dependency should apply this upgrade immediately.

critical

How Path Traversal happens in JavaScript i18n loaders and how to fix it

A path traversal vulnerability in `beta/js/i18n-chatrd.js` allowed attackers to manipulate the `lang` URL query parameter to load arbitrary JSON files from the web server by injecting payloads like `../../sensitive-file`. The fix adds input validation to ensure only safe, expected language codes are accepted before they are interpolated into the fetch URL. This type of vulnerability is especially dangerous in internationalization loaders because they are often publicly accessible and designed to

high

How Path Traversal happens in Python FastAPI and how to fix it

A critical path traversal vulnerability was discovered in `SovitsTest/GSVI.py`, a FastAPI-based TTS inference server, where the `/upload` endpoint accepted user-supplied filenames without sanitization. An unauthenticated remote attacker could exploit this to write arbitrary files anywhere on the filesystem — including sensitive system directories like `/etc/cron.d`. The fix adds path validation to prevent filenames from escaping the intended upload directory.

high

How Path Traversal happens in Python Flask routes and how to fix it

A high-severity path traversal vulnerability was discovered in `xkeen-ui/routes/cores_status.py` at line 221, where user-controlled input was passed directly to Python's `open()` function without sanitization. An attacker could exploit this to read arbitrary files on the server by supplying crafted path strings like `../../etc/passwd`. The fix introduces strict path validation using a trusted root directory, ensuring only files within the intended directory can be accessed.

critical

How Local File Inclusion/Path Traversal happens in JavaScript PDF generation and how to fix it

CVE-2025-68428 is a critical Local File Inclusion/Path Traversal vulnerability in jsPDF versions prior to 4.0.0 that could allow attackers to read arbitrary files from the server's filesystem through unsanitized path inputs during PDF generation. The vulnerability was present in the `jspdf` dependency declared in `frontend/package-lock.json`, and was resolved by upgrading from version 3.0.4 to 4.0.0. Left unpatched, this flaw could expose sensitive server-side files to unauthorized access via cr

critical

How eval() Code Injection happens in JavaScript and how to fix it

A critical code injection vulnerability was discovered in `js/lib/jsencrypt.js` at line 195, where a direct `eval()` call executed a JavaScript string shim for the `process` object in browser environments. If an attacker could influence the string passed to `eval()`—through a compromised dependency, a man-in-the-middle attack, or supply chain tampering—they could achieve arbitrary JavaScript execution in any user's browser. The fix replaces the `eval()` call with the equivalent inline JavaScript