Skip to content

fix(server): stop serving siblings that share the cwd prefix - #2104

Merged
johanneswuerbach merged 1 commit into
testem:mainfrom
MaxFreedomPollard:fix/static-file-cwd-containment
Oct 26, 2026
Merged

johanneswuerbach merged 1 commit into
testem:mainfrom
MaxFreedomPollard:fix/static-file-cwd-containment

Conversation

@MaxFreedomPollard

Copy link
Copy Markdown
Contributor

Fixes #2016.

With unsafe_file_serving off, the dev server should only serve files inside cwd or behind a configured route. serveStaticFile() checked that with a substring test:

var isPathPermitted = filePath.indexOf(path.resolve(config.cwd())) !== -1;

Any resolved path that merely contains the cwd string passes. With cwd set to /tmp/app, a request for /%2E%2E%2Fapp-secret%2Fsecret.txt resolves to /tmp/app-secret/secret.txt and is served with a 200.

This PR checks containment with path.relative() instead, which is the check suggested in the issue. A request is rejected when the relative path is .., starts with .. plus the separator, or is absolute (another drive on Windows). These behave as before: files inside cwd (including names that start with two dots, such as ..foo.js), cwd itself, routed paths and unsafe_file_serving: true. Symlinks are still not resolved, as before.

Tests

Adds a describe block next to unsafe directory handling in tests/server_tests.js. It creates app/ and app-secret/ in a temp directory and starts the server with cwd set to app. It then checks that hello.js is served (200) and that %2E%2E%2Fapp-secret%2Fsecret.txt is rejected (403). The encoded request uses the same technique as the existing rejects a request for unsafe content test.

I could not run the mocha suite or eslint because npm dependencies could not be installed in my environment. Instead, I ran the real Server#serveStaticFile and Config on Node 22, with stand-ins for express, socket.io, compression, js-yaml and execa. A small node:http adapter passed serveStaticFile the same decoded path that the /*file route produces, and I sent the requests with Node's fetch:

Request Before After
hello.js (cwd = <tmp>/app) 200 200
%2E%2E%2Fapp-secret%2Fsecret.txt (cwd = <tmp>/app) 200, body secret 403
existing test: %2E%2E%2Fpublic%2F.eslintrc.js (cwd = tests) 403 403

I also called serveStaticFile directly. These cases are still served: ..foo.js inside cwd, cwd itself, a relative or trailing-slash cwd, / as cwd, routed paths and unsafe_file_serving: true. An absolute path elsewhere that contains the cwd string now gets 403 (it was served before). I also evaluated the new check with path.win32 for a forward-slash cwd (#1286), another drive and a drive root.

🤖 Generated with Claude Code

With `unsafe_file_serving` off, testem's dev server should only serve
files inside `cwd` (or behind a configured route). serveStaticFile() in
lib/server/index.js checked that with
`filePath.indexOf(path.resolve(config.cwd())) !== -1`, a substring test.
Any resolved path that merely contains the cwd string passed it. With
`cwd` set to `/tmp/app`, a request for
`/%2E%2E%2Fapp-secret%2Fsecret.txt` resolves to
`/tmp/app-secret/secret.txt` and was served with a 200.

Check containment with path.relative() instead. The result climbs out of
cwd when it is `..` or starts with `..` plus the separator, and stays
absolute when the target is on another Windows drive. Files inside cwd,
including names that begin with two dots such as `..foo.js`, routed
paths and `unsafe_file_serving: true` behave as before.

Fixes #2016

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@johanneswuerbach
johanneswuerbach merged commit 609a4e4 into testem:main Oct 26, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Path traversal in dev server static file serving can expose files in sibling prefix-matching directories

2 participants