fix(api): relax drive path validation, harden archive, rename export route#5412
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughMount archive downloads now use ChangesMounts API and archive handling
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant DriveMedia
participant MountsRouter
participant MountsService
participant stream_mounts_archive
DriveMedia->>MountsRouter: POST /mounts/files/export
MountsRouter->>MountsService: build archive work list
MountsService-->>MountsRouter: safe archive members
MountsRouter->>stream_mounts_archive: stream MountArchiveSource list
stream_mounts_archive-->>DriveMedia: archive download
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
api/oss/tests/pytest/unit/test_mounts_file_ops.py (1)
106-120: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winStrengthen the injection-focused assertions.
The parametrized cases (
'a"; DROP TABLE.zip',"a\nb.zip") target header-injection risk, but the test only checkslatin-1round-trip and presence offilename*=UTF-8''. It doesn't assert that",\, or control characters (\n) are actually absent from the quotedfilename=value — the specific guarantee_content_disposition_attachmentclaims to provide.✅ Proposed stronger assertion
def test_header_is_latin_1_safe_with_utf_8_filename(self, filename): header = _content_disposition_attachment(filename) header.encode("latin-1") assert "filename*=UTF-8''" in header + # The quoted ASCII fallback must never carry the raw quote/backslash/control chars. + quoted = header.split('filename="', 1)[1].split('"; filename*=', 1)[0] + assert '"' not in quoted and "\\" not in quoted and "\n" not in quotedapi/oss/src/core/mounts/service.py (2)
1119-1142: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDocstring still describes the old tuple return.
Line 1133 says
Returns \(files, next_cursor)`but the function now returns aMountFilePageobject (line 1129); callers must usepage.files/page.next_cursor`.📝 Proposed docstring fix
- paint is fast no matter how big the mount is. Returns `(files, next_cursor)`; `next_cursor` - is None once the listing is exhausted. + paint is fast no matter how big the mount is. Returns a `MountFilePage` with `.files` and + `.next_cursor`; `next_cursor` is None once the listing is exhausted.
1274-1301: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDocstring still describes the old tuple-mount shape.
The docstring says each mount is a
(mount_id, zip_prefix, source_path)tuple, but the parameter is nowList[MountArchiveSource](line 1278) with fieldarchive_prefix, notzip_prefix.📝 Proposed docstring fix
- Each mount is a ``(mount_id, zip_prefix, source_path)``: - ``source_path`` scopes it to a FOLDER within the mount ("" = the whole mount, for "download - all"); ``zip_prefix`` places its files under ``prefix/`` in the zip (e.g. "agent-files" for + Each mount is a `MountArchiveSource(mount_id, archive_prefix, source_path)`: + ``source_path`` scopes it to a FOLDER within the mount ("" = the whole mount, for "download + all"); ``archive_prefix`` places its files under ``prefix/`` in the zip (e.g. "agent-files" for the folded agent mount). Folder markers are skipped. Each work item is a
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 3bd18b06-6f4b-4ccb-91a1-708b4d0f1739
⛔ Files ignored due to path filters (1)
web/packages/agenta-api-client/src/generated/api/resources/mounts/client/Client.tsis excluded by!**/generated/**
📒 Files selected for processing (8)
api/oss/src/apis/fastapi/mounts/models.pyapi/oss/src/apis/fastapi/mounts/router.pyapi/oss/src/apis/fastapi/mounts/utils.pyapi/oss/src/core/mounts/dtos.pyapi/oss/src/core/mounts/service.pyapi/oss/tests/pytest/unit/test_mounts_file_ops.pyapi/oss/tests/pytest/unit/test_mounts_service.pyweb/oss/src/components/Drives/driveMedia.ts
Switch validate_file_path from a character allowlist to a denylist so real folder names (route groups, npm scopes, `c++`, `#`, `~`, non-ASCII) that the lazy-browse flow round-trips no longer 422 on every expand. Reject only unsafe input: absolute paths, empty/`.`/`..` segments, NUL and control characters. Harden the archive path in the same pass (opposite direction): validate ArchiveMount prefix/path, sanitize `..` out of zip entry names to prevent zip-slip, and make the Content-Disposition fallback ASCII-only so a non-latin-1 filename stops 500ing on Starlette's latin-1 header encoding. Claude-Session: https://claude.ai/code/session_01PTkjdoAPbhSEFR3ubpgzBU
"archive" is this router's soft-delete lifecycle verb (archive_mount two registrations below), and the collision is what forced the route-ordering hack. Rename the download-all zip route to the non-lifecycle /files/export and update the operation_id, handler, and all three frontend touchpoints (driveMedia streaming + buffered paths, and the hand-added client method, which stays hand-written until the Fern regen ticket). Claude-Session: https://claude.ai/code/session_01PTkjdoAPbhSEFR3ubpgzBU
`depth` advertised ge=1 but only depth==1 is implemented; depth=2 silently fell through to the most expensive full-tree browse branch. Constrain it to le=1 so unsupported depths 422. Type `order` as a Literal so a typo 422s at the boundary instead of returning the flat view unsorted. Claude-Session: https://claude.ai/code/session_01PTkjdoAPbhSEFR3ubpgzBU
Replace the archive path's anonymous `(mount_id, prefix, path)` triples with a MountArchiveSource DTO, per the typed-DTO convention. Thread it through the service, archive utils, router, and unit tests. (The companion MountFilePage DTO from the original change is dropped: the flat paged listing it named was removed with the Files drawer's flat view.) Claude-Session: https://claude.ai/code/session_01PTkjdoAPbhSEFR3ubpgzBU
`total` had three undocumented computed meanings; state the one contract explicitly (entries the queried view returns before any limit: leaf files in flat/recency modes, files-plus-folders in shallow/browse). Also stop the download-all docstring from promising >4 GB files, which are out of product scope. Claude-Session: https://claude.ai/code/session_01PTkjdoAPbhSEFR3ubpgzBU
test_mounts_service.py still asserted the old allowlist rejected angle brackets; under the denylist they are legitimate filename characters. Assert they (and route-group names) are accepted, and pin control-char rejection. Claude-Session: https://claude.ai/code/session_01PTkjdoAPbhSEFR3ubpgzBU
c93b418 to
d7d029c
Compare
dc98760 to
0feba68
Compare
Railway Preview Environment
|
Context
Part 2 of 2, stacked on #5411. These two PRs implement the pre-merge fixes agreed for #5400 ("[AGE-3965] refactor(drive): virtualize surfaces + unify Files drawer" by @ardaerzin) so it can merge on a sound base. #5411 carries the correctness fixes; this PR carries the contract, validation, and naming fixes. Its base is #5411's branch, so the diff here is only this PR's changes.
The headline is a validation bug that breaks #5400's own lazy-browse flow on real repositories.
validate_file_pathused a character allowlist,[\w. -]+per path segment, which rejects folders agents genuinely create: Next.js route groups likeapp/(auth)/[slug], npm scopes like@scope/pkg,c++.md, and names containing,,#,~, or non-ASCII characters. Listings surface those names (agents write them through prefix-scoped signed credentials, which bypass validation), but every round-trip endpoint (expand, read, download) validatespathand returned 422. So the file browser showed folders it could never open. The old UI fetched the whole tree in one call with nopath, so this rarely fired. The new lazy flow sendspathon every expansion, which promotes the latent restriction into a first-class product bug.Changes
Relax path validation from an allowlist to a denylist.
validate_file_pathnow rejects only genuinely unsafe input: absolute paths,./../empty segments, NUL, and control characters. Everything else, including the real-world folder names above, passes. Realistic-name tests pin the new behavior.Harden the archive inputs in the same pass. These changes pull the opposite direction from the relaxation above, so they belong in one commit.
ArchiveMount.pathandprefixare now validated. Zip members whose keys smuggle..or backslash traversal are skipped with a warning log instead of minting zip-slip entries that would escape the extraction directory on the client. TheContent-Dispositionfilename fallback is ASCII-only, so CJK and emoji filenames no longer 500 on the latin-1 header encode.Rename
POST /files/archivetoPOST /files/export. "Archive" is this router's own soft-delete verb, registered three lines below, and the collision was what forced a route-ordering hack. The rename removes the ambiguity and the fragile ordering together. All three frontend call sites are updated. Renaming after the frontend ships against/archivewould be a breaking change, so it lands now.Constrain the listing parameters.
depthgetsle=1(the service only implementsdepth == 1;depth=2silently fell through to the most expensive full-tree branch).orderbecomes aLiteral(any other string previously returned the flat view unsorted with no error). Invalid values now return 422 instead of silently choosing the wrong or most expensive path.Name the anonymous tuple. The archive path's
(UUID, str, str)triple becomes aMountArchiveSourceDTO, threaded through the service, archive utils, router, and tests.api/AGENTS.mdnames bare-tuple returns as an anti-pattern, and every sibling method here returns a DTO. (The companionMountFilePageDTO from the original change is dropped: the flat paged listing it named was removed with the Files drawer's flat view in387c62ebef.)Document
totaland soften the archive docstring.totalcounts the entries a view returns before any limit, but its unit follows the view: leaf files only in the recency listing (order and limit set), files-plus-folders in the shallow (depth=1) and browse modes. That per-view meaning is now documented. The archive docstring no longer claims to handle files over 4 GB, which the product owner has ruled out of scope.Verification
Live QA (recorded). A wire matrix and 60-second UI recording were run on an isolated docker stack against the pre-
387c62ebeftip: special-character folder paths returning 200, constraint violations returning 422, export returning 404 on a bad mount, valid zips downloading, the CJK header staying safe, and the old/archiveroute being gone. All of these target surviving code and remain valid. The recording is attached as a comment on the base PR #5411 (the canonical copy for both PRs).Tests. The mounts file-ops suite covers the relaxed validation, the export rename, the parameter constraints, and the zip-slip skip (
uv run pytest oss/tests/pytest/unit/test_mounts_file_ops.py -q). The full API unit suite is green on the rebased tip.Independent review. Codex reviewed this diff at high reasoning effort. Its one confirmed catch on this branch, a backslash-based zip-slip path, is fixed and regression-tested.
Merge order
Merge #5411 into
fe-refactor/drive-surfacesfirst, then this PR into #5411's branch, then #5400 intomain. Merging is Mahmoud's decision. Ready for review, do not merge without Mahmoud's go.https://claude.ai/code/session_01PTkjdoAPbhSEFR3ubpgzBU