fix(designer): confine file-content API to loaded project roots - #2301
Open
fra-shipper wants to merge 2 commits into
Open
fra-shipper wants to merge 2 commits into
fra-shipper wants to merge 2 commits into
Conversation
get_file_content_endpoint and save_file_content_endpoint in file_content/mod.rs read and write a client-supplied file_path with fs::read_to_string / fs::write and no validation. The designer server binds 0.0.0.0:49483 with any-origin CORS and no authentication, so an unauthenticated caller could read or write any file the tman process has access to (e.g. /etc/passwd, ~/.ssh/authorized_keys) via an absolute path or a `..` traversal. Add ensure_within_allowed_roots(), which canonicalizes the requested path and rejects it unless it resolves inside one of the app base directories already tracked in DesignerState.pkgs_cache. The read endpoint validates the target file directly; the write endpoint validates the (now-created) parent directory first, since fs::canonicalize requires the path to exist, then joins the file name back on. Both endpoints return 403 Forbidden on a rejected path, and 400 Bad Request is preserved for pre-existing malformed-path and I/O error cases. An empty pkgs_cache (no app loaded yet) is treated as "no allowed roots" and rejects every request. Adding authentication and binding to loopback by default are left as follow-ups; this change only closes the path-confinement gap. Adds unit tests under tests/test_case/designer/file_content/ covering: an in-root read/write round trip, a read/write of an absolute path outside any loaded root, a `..` traversal that resolves outside the root, and requests made with no project root loaded.
save_file_content_endpoint called fs::create_dir_all(parent) on the raw, attacker-supplied file_path before ensure_within_allowed_roots ever ran, so an unauthenticated caller could make it mkdir -p an arbitrary directory chain outside every loaded project root before the write itself was correctly rejected with 403. Add resolve_prospective_path, which walks up to the nearest existing ancestor of the target parent, canonicalizes it, and re-applies the remaining not-yet-created components on top before any directory is created, and confine the write to that validated path. This also closes a related escape: since create_dir_all re-resolves any `..` left in its argument against directories it creates along the way, validating only the final, already-created parent (as before) does not stop a crafted path like `a/b/../../../etc` from making it create a directory outside of `a`. Also split PathRejection into Unresolvable vs OutsideAllowedRoots in ensure_within_allowed_roots, so get_file_content_endpoint can tell a merely non-existent path (400, matching pre-PR behavior) apart from one that resolves outside every loaded root (403), instead of bucketing every canonicalize failure into a security rejection. Adds a save test where the out-of-root target's parent chain does not exist yet (the prior tests' parents already existed, so create_dir_all was a no-op and never exercised the ordering bug), asserting no directory is created under the outside root, and a get test for the 400-vs-403 distinction.
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
Refs #2187 -- addresses the path-confinement half of the report. See "Not addressed" below for what this PR does not cover.
get_file_content_endpointandsave_file_content_endpointincore/src/ten_manager/src/designer/file_content/mod.rsread and wrote a client-suppliedfile_pathwithfs::read_to_string/fs::writeand no validation. The designer server binds0.0.0.0:49483with any-origin CORS and no authentication middleware, so an unauthenticated caller could read or write any file thetmanprocess has access to (e.g./etc/passwd,~/.ssh/authorized_keys) via an absolute path or a..traversal, matching the PoC in the issue.Fix
ensure_within_allowed_roots(), which canonicalizes the requested path and rejects it unless it resolves inside one of the app base directories already tracked inDesignerState.pkgs_cache. Both endpoints return403 Forbiddenwhen a path is rejected outside the allowed roots;400 Bad Requestis preserved for the pre-existing malformed-path/I/O-error cases (aPathRejectionenum keeps a merely non-existent path from being bucketed with a confinement violation).fs::create_dir_all(parent)on the raw, attacker-supplied path before any validation ran, so a request could make it create directories outside every loaded root even though the write itself was rejected afterwards. Addedresolve_prospective_path(), which walks up to the nearest existing ancestor of the target parent, canonicalizes only that, and lexically re-applies the not-yet-created suffix on top -- all beforecreate_dir_allruns. This also closes a related escape:create_dir_allre-resolves any..left in its argument against directories it creates along the way, so validating only the already-created parent (the naive fix) does not stop a crafted path likea/b/../../../etc(where onlyaexists and is an allowed root) from creating a directory outside ofa.pkgs_cache(no app loaded yet) is treated as "no allowed roots" and rejects every request rather than allowing or denying by an unvalidated default.Not addressed
The issue's "Expected behavior" also asks for authentication on the designer API and binding to loopback by default instead of
0.0.0.0. Neither is in this diff; it is scoped to path confinement. An unauthenticated caller can still reach these endpoints -- the fix limits what such a caller can read or write to files inside a currently loaded project root, instead of the whole filesystem.Testing
Added 9 unit tests under
core/src/ten_manager/tests/test_case/designer/file_content/mod.rs, following the existing pattern indesigner/dir_list: in-root read/write round trip, read/write of an absolute path outside any loaded root, a..traversal that resolves outside the root (both endpoints), a request made with no project root loaded, and a save whose target parent directory chain does not exist yet on disk (the case that exercises thecreate_dir_all-ordering bug -- the other tests' parents already existed, socreate_dir_allwas a no-op there).I don't have a Rust toolchain available in my normal working environment for this repo (
ten_manager's dependency tree includesclingo-sys, which compiles the Clingo ASP-solver C++ library from source viacmake, andcmake/bison/re2cweren't installed). I later got a toolchain working long enough to build and run this crate's tests directly:To confirm the new tests actually exercise this fix (not just compile), I reverted only
file_content/mod.rsto its pre-fix state (test file untouched) and re-ran the same command: 6 of the 9 tests failed withassertion left == right failed / left: 200 / right: 403against the endpoints that should have rejected the request. I then restored the fix and re-ran to the same 9/9 passing result shown above.cargo check -p ten_manageralso finished cleanly with 0 warnings.