fix(api): prevent path traversal in /tasks/file endpoint (CWE-22) - #230
Open
sebastionoss wants to merge 1 commit into
Open
fix(api): prevent path traversal in /tasks/file endpoint (CWE-22)#230sebastionoss wants to merge 1 commit into
sebastionoss wants to merge 1 commit into
Conversation
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.
Summary
The
GET /api/v1/tasks/fileendpoint inapps/backend/app/api/tasks.pyaccepts a user-suppliedpathquery parameter and passes it directly topathlib.Path(...)before reading the file contents. There is no validation that the resolved path stays inside the intended output directory, and the endpoint has no authentication (the router is mounted without anyDepends(...)gate andapps/backend/app/main.pyonly registersCORSMiddleware). Any network-reachable client can read arbitrary files that the backend process can access — including/etc/passwd, application config,.envfiles with API keys, private SSH keys, etc.apps/backend/app/api/tasks.py,read_file()(routeGET /api/v1/tasks/file)Dockerfileexposes the API without auth)Data flow
path(query string, attacker controlled) →Path(path)→file_path.exists()/file_path.read_text()/FileResponse(file_path). No canonicalisation, no base-directory check, no allow-list.Fix
Canonicalise the requested path with
Path(...).resolve()and verify — viarelative_to(base_dir)— that it is contained withinsettings.OUTPUT_DIR(also resolved). Anything outside returns403; unresolvable paths return400. This blocks../traversal, absolute paths (/etc/passwd), and symlink escapes (becauseresolve()follows symlinks before the containment check).The change is 16 lines, localised to the one vulnerable handler, and preserves the legitimate use case (reading task outputs written under
OUTPUT_DIR).Proof of Concept
Against a local run of the backend (
uvicorn app.main:appfromapps/backend, defaultOUTPUT_DIR):Testing
/etc/passwd,../../etc/passwd, and other out-of-base inputs return403after the patch.OUTPUT_DIRstill returns200and file contents.ln -s /etc/passwd $OUTPUT_DIR/link; request?path=$OUTPUT_DIR/link) is blocked becauseresolve()follows the symlink prior to therelative_tocheck.Adversarial review
Before submitting we tried to disprove this. We checked whether the router or the FastAPI app applied any auth dependency that would make the endpoint unreachable to anonymous clients —
apps/backend/app/main.pymountstasks_routerwith only a prefix andCORSMiddleware, andAPIRouter(prefix="/tasks", ...)intasks.pyhas nodependencies=[Depends(...)]. We also considered whetherOUTPUT_DIRbeing an absolute path would defeat the check — it doesn't, because both sides areresolve()d before comparison. Finally we considered whether the endpoint might already be gated at deployment (reverse proxy, network policy); the shippedDockerfileexposes the API port directly with no auth layer, so the default deployment is exploitable.Discovered by the Sebastion AI GitHub App.