better-helperjs Path Traversal via String Prefix Bypass in Static Server
The production static file server in better-helperjs lets attackers read files from adjacent directories on disk by exploiting a flawed string prefix check that mistakes a shared directory name…
The problem
In versions up to and including 3.0.5, the safeStaticPath() function in src/ssr/site-server.ts validates a resolved file path using resolved.startsWith(root). That check treats the root directory string as a plain text prefix, not a filesystem boundary.
If the static root is /app/dist/client, the resolved path /app/dist/client-secrets/database.sqlite passes the check because the string literally starts with /app/dist/client. An unauthenticated remote attacker can therefore read any file in a sibling directory whose name begins with the same prefix as the configured static root.
This only triggers in NODE_ENV=production; the Vite dev middleware used during development is not affected.
Proof of concept
A working proof-of-concept for this issue in better-helperjs, with the exact payload below.
GET /%2e%2e%2fclient-secrets/secret.txt HTTP/1.1
Host: localhost:4174
Connection: closeThe percent-encoded %2e%2e%2f (../) is decoded before path.resolve() runs, so the server resolves the request to /app/dist/client-secrets/secret.txt. Because that string starts with the root /app/dist/client, the old startsWith(root) guard passes and the file is served.
The patch tightens the check to resolved.startsWith(root + path.sep) || resolved === root. Appending path.sep (/ on POSIX, \ on Windows) means a path must start with /app/dist/client/, so /app/dist/client-secrets/... no longer matches. The root cause is CWE-22: the validator used lexical string comparison instead of canonical path boundary comparison.
The fix
Upgrade better-helperjs to version 3.0.6 or later. The fix is in safeStaticPath(): the guard is now !resolved.startsWith(root + path.sep) && resolved !== root. If you cannot upgrade immediately, ensure no sensitive directories exist adjacent to your static build output directory that share the same name prefix (for example, avoid having both dist/client and dist/client-secrets on the same host).
Related research
- highFlowise: Authenticated Arbitrary File Write via S3 Directory Loader Path Traversal
- high · 7.1CVE-2026-54545CVE-2026-54545: @wakaru/cli Arbitrary File Write via Path Traversal in --unpack
- highCVE-2026-55607CVE-2026-55607: @anthropic-ai/claude-code Sandbox Escape via Git Worktree Path Confusion
- high · 7.5CVE-2026-15074CVE-2026-15074: @fastify/static Route Guard Bypass via Dot-Dot Path Traversal