Skip to content

Fix path traversal via double percent-encoded slash bypassing directory containment - #154

Open
iaohkut-from-NightWolf-Team wants to merge 1 commit into
11ty:mainfrom
iaohkut-from-NightWolf-Team:fix/double-encoded-path-traversal
Open

Fix path traversal via double percent-encoded slash bypassing directory containment#154
iaohkut-from-NightWolf-Team wants to merge 1 commit into
11ty:mainfrom
iaohkut-from-NightWolf-Team:fix/double-encoded-path-traversal

Conversation

@iaohkut-from-NightWolf-Team

Copy link
Copy Markdown

Fixes #152.

Summary

new URL()'s dot-segment removal neutralizes literal ../ and %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. A later decodeURIComponent() call then materializes a real ../ traversal after the URL layer's own normalization already ran, reaching isFileInDirectory()'s naive prefix check and escaping this.dir via a same-prefix sibling directory (e.g. _site-leak next to _site).

Fix

Reject any literal .. path segment that appears only after percent-decoding, closing the bypass at its actual point of origin:

computedPath = decodeURIComponent(computedPath);

if(computedPath.split(path.sep).includes("..")) {
  throw new Error("Invalid path");
}

Note on scope: I initially tried fixing isFileInDirectory()'s prefix-boundary check directly, but that function is also intentionally relied on (via the same prefix-matching behavior) by the unrelated directory.html-as-sibling-of-directory/ URL resolution feature — changing its semantics broke 6 existing tests. This targeted fix closes the actual exploit mechanism (the post-decode traversal segment) without touching that other feature.

Verification

  • Full existing test suite: 32/32 passing, no regressions.
  • Unit-level PoC:
    server.getOutputDirFilePath("%2e%2e%2f_site-leak%2fleak.txt")
    // before: returns the out-of-bounds path
    // after:  throws Error("Invalid path")

See #152 for the full report and original reproduction (note: the original end-to-end curl reproduction there also demonstrates the separate unhandled-exception crash from #150 — this PR and that one are complementary and both needed for full robustness).

…ry containment (CWE-22)

new URL()'s dot-segment removal neutralizes literal ../ and %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. A later
decodeURIComponent() call then materializes a real ../ traversal after the
URL layer's own normalization already ran, reaching isFileInDirectory()'s
naive prefix check and escaping this.dir via a same-prefix sibling directory
(e.g. `_site-leak` next to `_site`).

Reject any literal `..` path segment that appears only after percent-decoding,
closing the bypass at its actual point of origin without touching
isFileInDirectory() itself (which a separate, unrelated feature — the
`directory.html` sibling-of-directory URL resolution — intentionally relies
on for its own same-prefix matching behavior; changing that function's
semantics broke 6 existing tests when tried, hence the more targeted fix here).

Verified against the existing test suite (32/32 passing) plus a PoC that
previously read a file from a sibling directory outside the served root.

Co-Authored-By: iaohkut <thb2601@gmail.com>
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 via double percent-encoded slash bypasses directory containment check

2 participants