Path traversal via double percent-encoded slash bypasses directory containment check
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 109
- Forks
- 20
- Avg merge
- 1m
- Merged PRs (30d)
- 1
Description
Summary
isFileInDirectory()'s containment check uses a naive string-prefix test with no trailing-separator boundary, so a sibling directory whose name happens to share a prefix with the served directory (e.g. _site-leak next to _site) is incorrectly treated as "inside" it. This is normally unreachable over HTTP because new URL()'s dot-segment removal neutralizes literal ..//%2e%2e/ sequences — but if the path separator itself is also percent-encoded (%2f), the whole segment survives new URL() untouched as one opaque path component, and a later decodeURIComponent() call materializes a real ../ traversal after the containment check's path has already been constructed.
CWE: CWE-22 (Path Traversal)
Severity: Medium
CVSS: 5.3 — CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N (AC:H because exploitation depends on a sibling directory/file existing whose name happens to share the served directory's name as a literal prefix)
Root Cause
server.js:282-284:
isFileInDirectory(dir, file) {
let absoluteDir = TemplatePath.absolutePath(dir);
let absoluteFile = TemplatePath.absolutePath(file);
return absoluteFile.startsWith(absoluteDir); // no trailing separator check
}
Reproduction
mkdir -p /tmp/poc/_site /tmp/poc/_site-leak
echo "SIBLING-DIR-SECRET" > /tmp/poc/_site-leak/leak.txt
# start server on /tmp/poc/_site as in issue #150
$ curl http://localhost:PORT/%2e%2e%2f_site-leak%2fleak.txt
SIBLING-DIR-SECRET
Unit-level confirmation of the underlying primitive:
isFileInDirectory("/tmp/poc/_site", "/tmp/poc/_site-leak/leak.txt") // → true (wrong)
A single-level escape to a path that does not share the served directory's name prefix (e.g. /%2e%2e%2fsecret.txt reaching straight outside with no naming coincidence) is correctly rejected — this bug's impact is bounded to prefix-sharing siblings, not arbitrary filesystem read.
Recommended Fix
isFileInDirectory(dir, file) {
let absoluteDir = TemplatePath.absolutePath(dir);
let absoluteFile = TemplatePath.absolutePath(file);
return absoluteFile === absoluteDir || absoluteFile.startsWith(absoluteDir + path.sep);
}
Verification
Dynamically confirmed on v3.0.0-alpha.11 against a real running server instance with real curl requests, as shown above.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at server.js:282-284 and inspect isFileInDirectory(), then reproduce the reported sibling-directory case with the provided /tmp/poc setup and curl request. Confirm that the containment check rejects /tmp/poc/_site-leak while preserving valid paths inside /tmp/poc/_site; the issue's unit-level call provides an additional verification point.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- security
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 82/100