fix(preview): prevent path traversal in serve-preview file serving#10424
fix(preview): prevent path traversal in serve-preview file serving#10424davidfirst wants to merge 4 commits into
Conversation
serveLocalPreview built the served file path as join(publicDir, filePath) where filePath
comes from the untrusted request URL, then res.sendFile(file) with an absolute path and no
{ root }. Since path.join collapses '..', a request like /msg/org/comp/../../../../etc/passwd
escaped publicDir and read arbitrary files.
Add resolvePreviewFilePath(publicDir, filePath) which returns the path only when it stays
within publicDir (else undefined -> 403), and use it before existsSync/sendFile. Unit-tested.
Code Review by Qodo
1. Dot path sends directory
|
PR Summary by QodoFix serve-preview path traversal by validating request-derived file paths WalkthroughsDescription• Block path traversal in serve-preview by rejecting file paths escaping the public directory. • Add a dedicated resolver utility to safely validate request-derived file paths. • Cover normal, nested, traversal, and prefix-sibling edge cases with unit tests. Diagramgraph TD
A(["HTTP client"]) --> B["PreviewMain: serveLocalPreview"] --> C["resolvePreviewFilePath()"]
C -->|"valid"| D{"Exists on disk?"} -->|"yes"| E["res.sendFile()"]
D -->|"no"| F["404 File not found"]
C -->|"invalid (escapes root)"| G["403 Invalid file path"]
subgraph Legend
direction LR
_actor(["Actor"]) ~~~ _proc["Process"] ~~~ _dec{"Decision"}
end
High-Level AssessmentThe following are alternative approaches to this PR: 1. Use Express sendFile with { root: publicDir }
2. Serve preview assets via serve-static middleware scoped to publicDir
Recommendation: The PR’s approach (resolve + enforce resolved path within publicDir) is solid and easy to audit, and it explicitly handles the sibling-prefix edge case (e.g., public vs public-evil). If future refactors touch this area, consider switching to sendFile({ root }) or serve-static to further reduce custom path-handling surface area, but the current fix is appropriate and well-tested. File ChangesBug fix (2)
Tests (1)
|
Code Review by Qodo
1. Symlink escape still possible
|
|
Code review by qodo was updated up to the latest commit 78d43e4 |
resolvePreviewFilePath now realpath()s both publicDir and the target so a symlink inside publicDir cannot redirect the served file outside it (lexical path.resolve alone doesn't follow symlinks). Falls back to the lexical resolve for not-yet-existent targets. Adds symlink unit coverage. Addresses Qodo review.
|
Code review by qodo was updated up to the latest commit ad7980b |
|
Re: Qodo's "Symlink escape still possible" finding — fixed in ad7980b. Context on severity: in serve-preview's threat model the HTTP/LAN client only controls the request path and can't plant a symlink in |
serveLocalPreviewbuilt the served file path asjoin(publicDir, filePath), wherefilePathis the trailing portion of the untrusted request URL (/:message/:componentId(*)), then calledres.sendFile(file)with an absolute path and no{ root }. Becausepath.joincollapses.., a request like/msg/org/comp/../../../../etc/passwd(or percent-encoded) resolved outsidepublicDirand letsendFilereturn arbitrary files readable by the process.Adds
resolvePreviewFilePath(publicDir, filePath)which returns the path only when it stays withinpublicDir(elseundefined→ 403), used before the existingexistsSync/sendFile. Allows legitimate nested files (assets/x.js), rejects..escapes and sibling-prefix dirs. Unit-tested inresolve-preview-file-path.spec.ts.Server bind address is intentionally left unchanged (same reasoning as the cloud login-listener: not narrowing a server's reachability here).