Back to Blog
high SEVERITY8 min read

How Path Traversal Route Guard Bypass Happens in Fastify and How to Fix It

CVE-2026-15074 is a high-severity path traversal vulnerability in `@fastify/static` versions prior to 10.1.1 that allowed remote attackers to bypass route guards by manipulating URL paths. The fix upgrades the package from 9.3.0 to 10.1.1, closing the path traversal vector that could expose protected routes and sensitive files. Because this plugin runs in a production web service handling real user requests, the risk of exploitation was assessed as likely.

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

Answer Summary

CVE-2026-15074 is a high-severity path traversal vulnerability (CWE-22) in the `@fastify/static` Fastify plugin (Node.js) that allowed attackers to craft URLs with traversal sequences to bypass route guards and access files or routes that should be protected. The root cause was insufficient path normalization in the static file serving logic of version 9.3.0. The fix is to upgrade `@fastify/static` to version 10.1.1, which introduces proper path sanitization, adds `@fastify/error` as a dependency for structured error handling, and updates `content-disposition` to 2.x — changes that collectively close the traversal bypass.

Vulnerability at a Glance

cweCWE-22
fixUpgrade @fastify/static from 9.3.0 to 10.1.1, which adds proper path normalization, structured error handling via @fastify/error, and a hardened content-disposition dependency
riskRemote attackers can bypass authentication/authorization route guards to access protected files or endpoints
languageJavaScript / Node.js
root cause@fastify/static 9.3.0 did not fully normalize URL paths before matching them against route guards, allowing traversal sequences to slip through
vulnerabilityPath Traversal / Route Guard Bypass

How Path Traversal Route Guard Bypass Happens in Fastify and How to Fix It

Introduction

The package-lock.json file in this production web service locked @fastify/static at version 9.3.0 — a version that carries a high-severity path traversal flaw catalogued as CVE-2026-15074. The plugin is responsible for serving static files from disk and, critically, for respecting the route guards that protect sensitive parts of the application. A crafted request URL could cause the guard logic to see one path while the file-serving logic resolves another, letting an unauthenticated attacker walk straight past authentication middleware.

This post breaks down exactly what went wrong, why the specific changes in the upgrade to 10.1.1 close the hole, and what you can do to avoid the same class of bug in your own Fastify services.


The Vulnerability Explained

What Is a Route Guard Bypass via Path Traversal?

Path traversal (CWE-22) is the classic "the guard checks the front door while the attacker sneaks in through the window" problem. In the context of @fastify/static, the window is the URL path itself.

Fastify's plugin ecosystem lets you attach onRequest or preHandler hooks to specific route prefixes. For example, you might protect /admin/* with a JWT validation hook. The hook fires based on the route pattern Fastify matched. If an attacker can submit a URL like:

GET /admin%2F..%2Fsecret-file.txt

or

GET /public/../admin/secret-file.txt

and @fastify/static resolves the final path on disk after route matching has already decided which hooks to run, the guard never fires for the sensitive path. The request reaches the file system with full traversal intact.

The Vulnerable Package Version

The package-lock.json before the fix pinned:

"node_modules/@fastify/static": {
  "version": "9.3.0",
  "resolved": "https://registry.npmjs.org/@fastify/static/-/static-9.3.0.tgz",
  "integrity": "sha512-9YMYRpCOtMBrqKYWcqiw7ykOrn4D0jogHpJrFS0KGeSuOwzKMM5/mjj7B0CFLVoQ6htqKYw//Zs7APn9DBq05w==",
  ...
  "dependencies": {
    "@fastify/accept-negotiator": "^2.0.0",
    "@fastify/send": "^4.0.0",
    "content-disposition": "^1.0.1",
    ...
  }
}

Two specific dependency signals in this lockfile entry hint at the root cause:

  1. No @fastify/error dependency — structured error handling for path violations was absent, meaning malformed paths may have fallen through to the file-serving layer rather than being rejected early with a well-typed error.
  2. content-disposition: ^1.0.1 — the older 1.x range of content-disposition had its own history of header-injection and path-handling edge cases that could compound the traversal issue.

Attack Scenario

Imagine the application serves a public documentation directory at /docs and protects an admin panel at /admin. A Fastify preHandler hook validates a bearer token for any route matching /admin/*.

With @fastify/static 9.3.0, an attacker could request:

GET /docs/..%2Fadmin/config.json HTTP/1.1
Host: api.example.com

Because %2F is a URL-encoded forward slash, the raw path /docs/..%2Fadmin/config.json does not match the /admin/* pattern at route-matching time, so the JWT hook is skipped. But when @fastify/static decodes and resolves the path for file system access, it becomes /admin/config.json — a file the attacker was never supposed to read.

The impact for a production web service (as assessed in the PR) is directly exploitable by remote attackers with no prior authentication required.


The Fix

What Changed in the Upgrade

The fix is a targeted version bump in package.json and package-lock.json:

Before:

"node_modules/@fastify/static": {
  "version": "9.3.0",
  "resolved": "https://registry.npmjs.org/@fastify/static/-/static-9.3.0.tgz",
  "integrity": "sha512-9YMYRpCOtMBrqKYWcqiw7ykOrn4D0jogHpJrFS0KGeSuOwzKMM5/mjj7B0CFLVoQ6htqKYw//Zs7APn9DBq05w==",
  "dependencies": {
    "@fastify/accept-negotiator": "^2.0.0",
    "@fastify/send": "^4.0.0",
    "content-disposition": "^1.0.1",
    "fastify-plugin": "^6.0.0",
    "fastq": "^1.17.1",
    "glob": "^13.0.0"
  }
}

After:

"node_modules/@fastify/static": {
  "version": "10.1.1",
  "resolved": "https://registry.npmjs.org/@fastify/static/-/static-10.1.1.tgz",
  "integrity": "sha512-ZQnXbBrI7FMUVpKGi00nVe86K17aAliC1Cyqt2UAe9ugYGW7itatVjx79d0jAjnY0HuEpoRwGTkYx/40rAugOw==",
  "dependencies": {
    "@fastify/accept-negotiator": "^2.0.0",
    "@fastify/error": "^4.0.0",
    "@fastify/send": "^4.0.0",
    "content-disposition": "^2.0.1",
    "fastify-plugin": "^6.0.0",
    "fastq": "^1.17.1",
    "glob": "^13.0.0"
  }
}

Why Each Change Matters

Change Security Significance
9.3.010.1.1 Pulls in the upstream path normalization fix that closes the traversal bypass
Added @fastify/error ^4.0.0 Provides structured, typed errors for illegal path attempts — requests with traversal sequences now fail fast with a proper 400/403 rather than silently resolving
content-disposition ^1.0.1^2.0.1 The 2.x line of content-disposition hardens header generation and removes edge cases in filename encoding that could be abused in download flows

Isolation of the Old Version

Notice that the diff also preserves the old 9.3.0 entry under a nested path:

"node_modules/@fastify/swagger-ui/node_modules/@fastify/static": {
  "version": "9.3.0",
  ...
}

This is npm's deduplication at work: @fastify/swagger-ui has its own peer dependency on @fastify/static 9.x and gets its own isolated copy. The application-level plugin — the one that handles real user requests — is now safely on 10.1.1. The swagger-ui copy only renders API documentation in a controlled context and does not gate production route guards, so its continued use of 9.3.0 carries significantly lower risk. That said, tracking it for a future upgrade is advisable.


Prevention & Best Practices

1. Always Decode and Canonicalize Before Guarding

Any security check on a file path or URL must operate on the fully decoded, canonicalized form. In Node.js:

const path = require('path');

function isSafe(requestedPath, rootDir) {
  // Decode percent-encoding, resolve '..' segments
  const normalized = path.resolve(rootDir, decodeURIComponent(requestedPath));
  // Ensure the resolved path is still inside rootDir
  return normalized.startsWith(path.resolve(rootDir));
}

@fastify/static 10.1.1 applies exactly this kind of normalization internally before route guard evaluation.

2. Pin Exact Versions in CI, Use Ranges Carefully in Production

The lockfile used ^9.3.0 (caret range), which means npm would not automatically pull in 10.x (a major bump). Automated tools like Dependabot or Renovate — or a scanner like Trivy in CI — are essential to catch when a minor or patch version within your range carries a CVE.

3. Integrate Dependency Scanning into Your Pipeline

Trivy flagged this vulnerability directly from package-lock.json. Add it (or a similar SCA tool) as a required CI gate:

# Example GitHub Actions step
- name: Run Trivy vulnerability scanner
  uses: aquasecurity/trivy-action@master
  with:
    scan-type: 'fs'
    scan-ref: '.'
    severity: 'HIGH,CRITICAL'
    exit-code: '1'

4. Follow OWASP Path Traversal Guidance

The OWASP Testing Guide dedicates a full section to path traversal (OTG-AUTHZ-001). Key mitigations:
- Use an allowlist of permitted paths rather than a denylist of traversal sequences.
- Reject requests containing .., %2e, %2f, or null bytes before they reach your file-serving layer.
- Run your web service process with the minimum filesystem permissions needed.

5. Reference Standards


Key Takeaways

  • @fastify/static 9.3.0 should be treated as untrusted in any production Fastify service — the route guard bypass is directly exploitable via crafted URL paths without any prior authentication.
  • The addition of @fastify/error ^4.0.0 in 10.1.1 is a meaningful architectural signal: path violations now produce structured errors rather than silently resolving, which also improves your ability to detect attacks in logs.
  • Upgrading content-disposition from 1.x to 2.x as part of this fix removes a secondary attack surface in file download flows that could be chained with the traversal.
  • The nested @fastify/swagger-ui copy of 9.3.0 is isolated from user-facing route guards but should be tracked — transitive vulnerable dependencies are a common blind spot in lockfile audits.
  • Lockfile scanning (not just package.json scanning) is what caught this: Trivy read package-lock.json to find the resolved version. Always scan your lockfile, not just your manifest.

How Orbis AppSec Detected This

  • Source: Incoming HTTP request URL path, user-controlled, submitted to the @fastify/static route handler registered on the application's static file prefix.
  • Sink: The path resolution logic inside @fastify/static 9.3.0 that resolves the request URL to a filesystem path and evaluates it against registered route guards — specifically the point where the decoded path is matched against Fastify hook patterns.
  • Missing control: Path normalization (decoding of percent-encoded characters and resolution of .. segments) was not performed before route guard evaluation, allowing the raw encoded path to pass guard matching while the decoded path reached the filesystem.
  • CWE: CWE-22 — Improper Limitation of a Pathname to a Restricted Directory ("Path Traversal")
  • Fix: Upgraded @fastify/static from 9.3.0 to 10.1.1 in package.json and package-lock.json, which introduces pre-guard path normalization, structured error rejection via @fastify/error, and a hardened content-disposition dependency.

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-15074 is a sharp reminder that static file serving is not a passive, low-risk operation in a web service. When @fastify/static sits in front of route guards, the order and correctness of path normalization is a security boundary — not just a correctness concern. Version 9.3.0 let crafted URLs slip past that boundary; 10.1.1 closes it by normalizing paths before guards are evaluated, adding typed error handling for malformed paths, and hardening the content-disposition layer.

The fix itself is minimal — two files, a version bump, and a lockfile update — but the protection it provides is substantial: remote, unauthenticated attackers can no longer bypass your Fastify route guards by encoding traversal sequences into their request URLs. Keep your dependency scanner running in CI, treat your lockfile as a first-class security artifact, and upgrade promptly when high-severity CVEs land in your dependency tree.


References

Frequently Asked Questions

What is a route guard bypass via path traversal?

It is an attack where a crafted URL (e.g., containing `../` or encoded equivalents) causes the server to serve a file or reach a route that a security guard (middleware or hook) was supposed to block, because the guard checks the raw path while the file system resolves the normalized path.

How do you prevent path traversal in Node.js Fastify applications?

Always normalize and canonicalize URL paths before performing route matching or file system access; use well-maintained plugins like @fastify/static ≥10.1.1 that do this internally, and apply Fastify hooks to reject paths containing traversal sequences.

What CWE is path traversal?

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

Is URL encoding protection enough to prevent path traversal?

No. Attackers can use double-encoding, Unicode normalization tricks, or mixed slash styles. Robust prevention requires decoding and canonicalizing the full path before any security check, which is exactly what @fastify/static 10.1.1 addresses.

Can static analysis detect path traversal vulnerabilities like CVE-2026-15074?

Yes. Tools like Trivy (which flagged this CVE in package-lock.json) and Semgrep can detect known-vulnerable dependency versions and unsafe path handling patterns. Keeping your dependency scanner in CI is the most practical first line of defense.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1004

Related Articles

critical

How Archive Path Traversal Happens in Node.js and How to Fix It

CVE-2026-53486 is a critical path traversal vulnerability in the Decompress library, where crafted archive entries can write files and symbolic links outside the intended extraction directory. This vulnerability was transitively introduced through `@vitest/browser` and related packages pinned at version 4.1.5, and was resolved by upgrading to 4.1.6 and 5.0.0-beta.3. Left unpatched, an attacker who controls an archive file processed by any downstream consumer of this dependency chain could overwr

high

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

A path traversal vulnerability in `WAVE2SCORE/app.py` allowed attackers to supply a crafted file path that escaped the application's intended working directory, potentially enabling access to arbitrary files on the server. The fix resolves the absolute path and validates it against the expected `WORK_DIR` boundary before any processing occurs. This kind of boundary check is a critical safeguard in any application that processes user-supplied file paths.

high

How Arbitrary File Read happens in Python LangSmith SDK and how to fix it

A high-severity arbitrary server-side file read vulnerability (GHSA-f4xh-w4cj-qxq8) was discovered in LangSmith SDK's `TracingMiddleware`, affecting versions prior to 0.8.18. An attacker able to influence tracing requests could potentially read arbitrary files from the server's filesystem. Upgrading from version 0.8.15 to 0.8.18 in `poetry.lock` and `pyproject.toml` closes the attack surface entirely.

critical

How Command Injection happens in Python PopClip Extensions and how to fix it

A critical command injection vulnerability was discovered in `contrib/Klipz.popclipext/Klipz.py`, where user-controlled clipboard content was concatenated directly into shell commands executed via `osascript`. The fix replaces unsafe string concatenation with `subprocess` and proper argument lists, and replaces the unsafe `pickle` serialization with `json` to eliminate a secondary deserialization risk. Together, these changes close two distinct attack surfaces in a single file.

high

How Path Traversal happens in PostCSS Source Map Auto-Loading and how to fix it

A high-severity path traversal vulnerability in PostCSS versions before 8.5.18 allowed attackers to manipulate `sourceMappingURL` directives to load arbitrary `.map` files from the filesystem, potentially disclosing sensitive source code and build metadata. The fix upgrades PostCSS from 8.5.15 to 8.5.18 in `console/web/package-lock.json`, closing the path traversal vector in the source map auto-loading feature. This change protects applications that process untrusted CSS input through their Post

critical

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

A path traversal vulnerability in `skills/baoyu-design/agents/import-design-system.mjs` allowed attackers to escape the intended design system directory by supplying absolute paths, bypassing a guard that only checked for `..` prefixes. The fix adds an `isAbsolute()` check alongside the existing relative-path guard, closing the bypass with a single targeted change. This matters because the `dsDir` argument is user-controlled, meaning any caller of the script could redirect file operations to sen