fix(files): prevent path traversal in file serving endpoint#175
Open
sebastiondev wants to merge 1 commit into
Open
fix(files): prevent path traversal in file serving endpoint#175sebastiondev wants to merge 1 commit into
sebastiondev wants to merge 1 commit into
Conversation
Use Path.resolve() to canonicalize the requested path and Path.is_relative_to() for directory containment checks instead of string prefix matching, which could be bypassed with crafted directory names or ../ sequences.
|
|
Author
|
Thanks for flagging the CLA requirement. I've signed the Contributor License Agreement via the CLA assistant link. Please recheck the status when ready. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Vulnerability Summary
CWE-22 (Path Traversal) in
api/routers/files.py— theget_fileendpoint (GET /api/v1/files/{file_path:path}) allows an unauthenticated attacker to read arbitrary files from the server's filesystem.The endpoint accepts a user-controlled
file_pathparameter and constructs a filesystem path via:It then attempts to validate the path using
abs_path.relative_to(Path.cwd())andstartswith("output"). However, becauseabs_pathis never resolved (no.resolve()call), a path likeoutput/../../etc/passwdpasses both checks:relative_to(Path.cwd())succeeds because the path is still relative to cwd in terms of string components — it returnsoutput/../../etc/passwdstartswith("output")passes because the relative path string literally starts with"output"The path is then handed to
FileResponse, which follows the../components and serves the target file.Proof of Concept
With the API server running (default port 8000):
No authentication is required — the
/filesrouter is mounted without any auth dependencies, and theget_fileendpoint has noDepends()guards.Fix Description
The fix resolves both the constructed path and the current working directory to their canonical absolute forms using
.resolve(), which normalizes away all../sequences and follows symlinks. The directory containment check is then performed using Python'sPath.is_relative_to()on the resolved paths, which cannot be bypassed with traversal sequences.Specifically:
cwd = Path.cwd().resolve()— canonical base directoryabs_path = (cwd / full_path).resolve()— canonical requested pathabs_path.is_relative_to(allowed_dir)— true containment check on resolved pathsThis replaces the flawed
relative_to()+startswith()approach, which operated on unresolved path components.Testing
We verified the fix by tracing the data flow from
file_paththrough path construction and validation:Path.cwd() / "output/../../etc/passwd"produces an unresolvedPathobject.relative_to(Path.cwd())returnsPosixPath('output/../../etc/passwd'), andstr(...).startswith("output")isTrue. The file is served.(Path.cwd() / "output/../../etc/passwd").resolve()produces/etc/passwd.Path("/etc/passwd").is_relative_to(Path.cwd() / "output")isFalse. The request is correctly rejected with 403.Legitimate requests like
GET /api/v1/files/output/video.mp4continue to work because the resolved path remains within the allowed directory.Adversarial Review
Before submitting, we considered whether existing protections would prevent exploitation. The
relative_to()+startswith()check appears to be a security gate but operates on unresolved path objects, so../sequences are preserved verbatim in the string representation. There is no authentication middleware on this router (confirmed by inspectingapi/app.pyline 133 and the router definition), no WAF or reverse proxy assumed in the default deployment, andFileResponsefaithfully serves whatever path it receives. The vulnerability is exploitable with a single HTTP request against any default deployment.Submitted by Sebastion — autonomous open-source security research from Foundation Machines. Free for public repos via the Sebastion AI GitHub App.