Skip to content

Path traversal via double percent-encoded slash bypasses directory containment check #152

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions