Back to Blog
high SEVERITY4 min read

Express `app.get('*')` Wildcard Handler Path Traversal in watch.js

A first-party Express server's wildcard route handler used `req.url.indexOf('font.woff2')` to gate access to a font file, allowing attackers to bypass the substring check with crafted paths. The fix replaces the catch-all handler with explicit route registration.

O
By Orbis AppSec
•Published September 27, 2026•Reviewed September 27, 2026

Answer Summary

First-party Express code using `app.get('*')` with a conditional `indexOf` check on `req.url`. An attacker could read arbitrary files by crafting URLs containing `font.woff2` as a substring while embedding path traversal sequences to escape the `./dist` root. The fix registers `app.get('/font.woff2')` as a dedicated route before the wildcard handler, eliminating the substring-based validation entirely. CWE-22.

Vulnerability at a Glance

cweCWE-22 (N/A assigned)
fixReplace conditional wildcard handler with explicit route registration
riskArbitrary file read via URL manipulation
languageJavaScript (Node.js/Express)
root causeSubstring check on `req.url` instead of strict route matching
vulnerabilityPath Traversal

Affected Versions

Affected not applicable (first-party code)
Fixed in not applicable (first-party code) — see fix commit
Ecosystem npm (Express framework)
CVE / GHSA not assigned
CWE CWE-22 (Improper Limitation of a Pathname to a Restricted Directory)

The Vulnerability Explained

The vulnerable code used Express's wildcard route handler app.get('*') to process all incoming GET requests through a single function. Inside this handler, a conditional check attempted to identify font file requests:

app.get("*", (req, res) => {
  if (req.url.indexOf("font.woff2") > 0) {
    res.sendFile("sketchybar-app-font.woff2", {root: "./dist"});
    return;
  }
  res.send(getPreviewHTML());
});

The critical flaw: req.url.indexOf('font.woff2') > 0 is not a path validation mechanism. This substring check has two fatal weaknesses:

  1. Position sensitivity: The > 0 check excludes position 0, but an attacker can place font.woff2 anywhere else in the URL
  2. No path normalization: The check operates on raw URL strings, not resolved filesystem paths

An attacker could craft requests like:
- GET /../../../etc/passwd?unused=font.woff2 — font.woff2 appears at position 28, satisfying the check while the actual path traversal targets arbitrary files
- GET /malicious/../../../font.woff2 — embedding the target substring within a traversal sequence

The {root: './dist'} option to res.sendFile() does prevent some traversal, but combined with the substring bypass, attackers could potentially reach unintended destinations depending on how Express resolves paths when the root option is combined with traversal sequences in the effective pathname.

The Fix

The remediation replaces the conditional logic with Express's native routing system:

app.get("/font.woff2", (req, res) => {
  res.sendFile("sketchybar-app-font.woff2", {root: "./dist"});
});

app.get("*", (req, res) => {
  res.send(getPreviewHTML());
});

What changed and why:

Aspect Before After
Route matching Single wildcard with manual substring check Dedicated route + fallback wildcard
Validation indexOf > 0 on raw URL Express's built-in path matching
Execution path Always enters wildcard, conditionally branches Only enters font handler for exact match

Express's app.get('/font.woff2') performs strict path matching before your code executes. The framework normalizes incoming paths and rejects traversal sequences at the routing layer, never reaching your handler with malicious input. The wildcard handler now only receives requests that genuinely don't match defined routes.

Key Takeaways

  • Substring checks on URLs are not security boundaries: indexOf, includes, and similar string methods can be bypassed by embedding target strings in unexpected positions—query parameters, path segments, or encoded forms.

  • Express routes are your validation layer: Define explicit routes for static assets rather than inspecting req.url inside catch-all handlers. The framework's path parsing and normalization runs before your code.

  • The root option in sendFile is not absolute protection: While it restricts the base directory, interaction with unsanitized path components can still produce unexpected resolutions depending on framework behavior.

  • Order matters in Express route registration: The fix places the specific font route before the wildcard handler. Express matches routes in registration order—specific handlers must precede generic ones.

  • Return statements in security conditionals indicate fragile design: Needing return to prevent fall-through suggests the control flow mixes concerns. Separate routes eliminate this class of error.

How Orbis AppSec Detected This

Source: The req.url property on incoming HTTP requests

Sink: res.sendFile() invoked with {root: './dist'} and a filename derived from conditional logic on unsanitized URL input

Missing control: No path normalization, no strict equality validation, and reliance on substring presence rather than exact route matching

CWE: CWE-22 — Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

Fix: Replaced the conditional substring check within a wildcard route with a dedicated app.get('/font.woff2') route that leverages Express's built-in path validation before any file operations occur.

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

This vulnerability demonstrates how seemingly defensive code—checking for a filename before serving it—can introduce exactly the weakness it attempts to prevent. The indexOf check created a false sense of security while providing attackers a flexible injection point. The fix leverages the framework's strengths: Express designed route matching to solve this problem, and using it eliminates an entire category of path manipulation attacks. When handling static files, let your router validate paths rather than implementing validation in application code.

Prevention and further reading

Frequently Asked Questions

Why did `req.url.indexOf('font.woff2') > 0` fail to protect against traversal?

The check only verified that `font.woff2` appeared somewhere in the URL, not that it was the actual filename. An attacker could use `../../../etc/passwd?font.woff2` or `/malicious/../../../font.woff2` to satisfy the substring check while traversing directories.

Does the fixed code change the behavior for legitimate font requests?

No. The dedicated `app.get('/font.woff2')` route handles the same file with identical `res.sendFile()` parameters, but now Express performs exact path matching before any code executes, eliminating the validation bypass window.

Why was `return` necessary in the vulnerable code but removed in the fix?

The original conditional needed `return` to prevent fall-through to `getPreviewHTML()`. The fixed architecture eliminates this need by separating concerns: the explicit font route handles its case, and the wildcard handler only runs for truly unmatched paths.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #508

Related Articles

high

updateCardBg() Follows Unvalidated 302 Location Headers

A background-image updater fetched a configured image URL with manual redirect handling and then re-issued the request to whatever `Location` header came back, with no scheme or host checks. A redirect to `http://169.254.169.254/` or `http://127.0.0.1:<port>/` would have been followed with the original fetch options attached, and the response body written to disk as an image asset. The fix resolves the redirect target against `imgDownloadUrl` and rejects anything that is not HTTPS on the same ho

high

markitdown_bridge.py Path Traversal: Arbitrary File Read via sys.argv

The markitdown_bridge.py script, used by MDView for DOCX-to-Markdown conversion, accepted file paths directly from command-line arguments without validating they stayed within intended directories. An attacker could exploit this to read arbitrary files from the filesystem by passing path traversal sequences in the source_path parameter.

high

SHACL Viewer Path Traversal in graph3d(): Unvalidated `path`

The `graph3d()` and `graph2d()` request handlers in SHACL Viewer directly concatenated user-supplied `path` parameters into filesystem paths, enabling directory traversal outside the intended `/shapes/` directory. The fix introduces `_resolve_shapes_path()` with `os.path.realpath()` validation to enforce containment within the shapes directory.

high

fs.readFileSync(process.argv[2]) Path Traversal in Zola Build

A build-time helper that extracts the expected SHA-256 for a downloaded Zola release passed `process.argv[2]` straight into `fs.readFileSync()` with no directory constraint, so any caller able to influence that argument could make the integrity check read an arbitrary file. The fix resolves the requested path and requires it to be a direct child of the tools directory, which is now passed in as an extra argument, and exits with an error otherwise. Because the bytes read become the "expected" che

high

write_page_jobs(): Unvalidated page_dir Escapes the Run Dir

The deck preparation runtime built per-page working directories by joining the `page_dir` string from a deck state document directly onto the run directory, with no containment check and no schema validation. A crafted or tampered deck record could therefore steer `page_request.json` writes anywhere on the filesystem the process could reach, including outside the run sandbox entirely. The fix routes both call sites through a single `page_dir_for(run_dir, page)` helper so the untrusted `page_dir`

high

Anthropic API Adapter Prototype Pollution in parseToolCallInput

The `parseToolCallInput` function in the Anthropic API adapter used `JSON.parse` without protecting against prototype pollution keys. An attacker who could manipulate API responses—through a man-in-the-middle attack, compromised Codex backend, or DNS spoofing—could inject `__proto__`, `constructor`, or `prototype` properties to pollute JavaScript's Object prototype and affect extension runtime behavior.