From 987fc73f047d896604bbf0b9365fe8538e7e29a0 Mon Sep 17 00:00:00 2001 From: iaohkut Date: Wed, 26 Aug 2026 01:33:51 +0700 Subject: [PATCH] Fix path traversal via double percent-encoded slash bypassing directory containment (CWE-22) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- server.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server.js b/server.js index d9de0c8..be95543 100644 --- a/server.js +++ b/server.js @@ -300,6 +300,14 @@ export default class EleventyDevServer { computedPath = decodeURIComponent(computedPath); + // Reject a literal `..` path segment that only appears after percent-decoding. + // This closes a bypass where a `%2f`-encoded separator survives the URL layer's + // own dot-segment normalization (which only recognizes literal `/`), then decodes + // into a real `../` here, escaping `this.dir` via a same-prefix sibling directory. + if(computedPath.split(path.sep).includes("..")) { + throw new Error("Invalid path"); + } + if(!filename) { // is a direct URL request (not an implicit .html or index.html add) let alias = this.matchPassthroughAlias(filepath);