fix: pre-auth path traversal in /static/* handler (GHSA-mc8w-wjhw-45x5) - #8081
Conversation
…wjhw-45x5) The /static/* handler (Minify.ts) converted backslashes to forward slashes unconditionally, *after* sanitizePathname() had already run. On POSIX a backslash is an ordinary filename byte, so sanitizePathname() deliberately leaves segments like `..\..\..` untouched (harmless there). The unconditional replace turned those already-sanitized bytes back into `../` traversal components with no re-check, giving any unauthenticated client an arbitrary file read: GET /static/plugins/ep_etherpad-lite/static/..%5C..%5C..%5Cetc/passwd The route is mounted on expressPreSession, before the auth middleware, so no credentials were required; disclosed settings.json/credentials.json/environ escalate to admin and, via the plugin installer, RCE. Guard the conversion to Windows only (`path.sep === '\\'`), matching the invariant already documented and enforced in sanitizePathname.ts. On POSIX the backslash bytes now stay literal and resolve to a non-existent filename (404). Adds a backend regression test that asserts the encoded-backslash traversal no longer discloses /etc/passwd while legitimate static assets are still served. Reported by @gcm-explo1t. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR Summary by QodoFix pre-auth /static/* path traversal via POSIX backslash handling (GHSA-mc8w-wjhw-45x5)
AI Description
Diagram
High-Level Assessment
Files changed (2)
|
Code Review by Qodo🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)
Great, no issues found!Qodo reviewed your code and found no material issues that require reviewTo customize comments, go to the Qodo configuration screen, or learn more in the docs. |
1 similar comment
Code Review by Qodo🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)
Great, no issues found!Qodo reviewed your code and found no material issues that require reviewTo customize comments, go to the Qodo configuration screen, or learn more in the docs. |
There was a problem hiding this comment.
Pull request overview
This PR fixes a pre-auth path traversal / arbitrary file read in Etherpad’s /static/* handler by preventing a post-sanitization backslash→slash normalization on POSIX, which could otherwise turn harmless ..\..\.. byte sequences into real ../ traversal components. It also adds a backend regression test to ensure the traversal payload is blocked while legitimate static assets still serve correctly.
Changes:
- Guard
filename.replace(/\\/g, '/')insrc/node/utils/Minify.tsso it only runs on Windows (path.sep === '\\'). - Add backend regression coverage for encoded-backslash traversal attempts against the plugin static handler.
- Include a sanity check that a normal plugin static asset is still served.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/node/utils/Minify.ts | Prevents POSIX backslash bytes from being transformed into / separators after sanitization (core vulnerability fix). |
| src/tests/backend/specs/staticPathTraversal.ts | Adds regression tests that assert traversal payloads 404 and a legitimate plugin asset still returns 200. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Headlines the GHSA-mc8w-wjhw-45x5 pre-auth arbitrary file read fix and documents the security fixes already on develop that 3.3.3 ships (GHSA-pp5v-mvwg-76mp, GHSA-73h9-c5xp-gfg4, GHSA-6mcx-x5h6-rpw2, GHSA-wg58-mhwv-35pq), plus the tsgo migration and editor/docker fixes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
src/tests/backend/specs/staticPathTraversal.ts:35
- This suite doesn't set a Mocha timeout. Many backend specs that call
common.init()setthis.timeout(30000)to avoid flakiness on slower CI or developer machines; without it, the default 2s timeout can intermittently fail before the server is ready.
describe(__filename, function () {
before(async function () { agent = await common.init(); });
Fixes GHSA-mc8w-wjhw-45x5 — unauthenticated path-traversal / arbitrary file read in the
/static/*handler (CVSS 9.8). Reported by @gcm-explo1t.Root cause
sanitizePathname.tsdeliberately leaves backslashes literal on POSIX (a\is a legal filename byte there), so a segment like..\..\..contains no./..path components and passes every traversal check unchanged — the repo's own testsanitizePathname.tsdocuments['posix', '..\\foo']as accepted-unchanged.Minify.tsthen ranfilename.replace(/\\/g, '/')unconditionally, after the sanitizer, with a comment claiming it was safe "because all..\\substrings have already been removed by sanitizePathname" — which is false on POSIX. That turned the already-sanitized bytes back into../traversal components after the check. Becausepath.join(pluginPath, libraryPath)produces an absolute path,path.resolve(ROOT_DIR, …)discardsROOT_DIRand surplus..collapses at/, so the payload is root-depth-agnostic:The route is mounted on
expressPreSession, beforesessionMiddleware/checkAccess, so no credentials are required.%5Cis the only working separator (%2Fdecodes to/and is rejected by the existing..guard). A reverse proxy does not mitigate it —%5Cforwards unchanged. Disclosedsettings.json/credentials.json//proc/self/environescalate to an admin session and, via the plugin installer, in-process RCE.Fix
Guard the backslash→slash conversion to Windows only (
if (path.sep === '\\')), restoring the invariant already documented and enforced insanitizePathname.ts. On POSIX the backslash bytes stay literal and resolve to a non-existent filename → 404. No behavior change for legitimate requests (the replace was already a no-op for valid POSIX paths). The sibling sink insanitizePathname.tsis already correctly Windows-guarded; theLIBRARY_WHITELISTbranch never did the replace.Test
Adds
src/tests/backend/specs/staticPathTraversal.ts:/etc/passwdis not disclosed/proc/self/cwd/settings.jsonpayload → 404Verified locally (Node 24): full backend suite 1614 passing; the 3 new specs pass. Reverting the one-line guard makes the
/etc/passwdtest fail with status 200. Live end-to-end reproduction against a prod server: fixed →404, 0 bytes; vulnerable control →200, 3686 bytesreturning/etc/passwd.Reported by @gcm-explo1t.