Repository navigation
Description
Summary
The id REPL's find/search file outputs build the local write path from the match's name verbatim. The confinement that closed #1614 — download_output_path in commands/find.rs, which reduces a match name to its final path component and refuses names with none — was applied only to the CLI handlers. The REPL has its own copies of the same path constructions, and they were never updated. A match named /home/user/.bashrc or ../victim.txt is written, with blob content, wherever the name points — in connected mode both the name and the content come from the remote node being queried, exactly the #1614 threat model, through the interactive surface instead of the one-shot CLI.
Same defect family as #1614 (CLOSED) and #1595: a file name reaching a filesystem path with no working containment check. This issue covers the REPL copies only; the CLI sites are fixed.
The chain (all on main 5a34fa70)
- Names arrive unchecked. In connected mode,
ReplContext::find(pkgs/id/src/commands/repl.rs,findat ~:696) returns the remote node'sMetaResponse::Findmatches postcard-decoded as-is;FindMatch.nameis a plainString. In local mode the names come from the local store, where blob rename still accepts traversal targets (id(fileops): rename targets accept traversal names (../../x) — store-side naming rule needed #1621, open). - Names become paths, five sites, all in
pkgs/id/src/repl/runner.rs:handle_find_commandsingle match (:769):args.output_file.unwrap_or(&m.name)— with--fileand no explicit output name, the match name IS the path; absolute names land anywhere.handle_search_commandfirst match (:834-835): the same construction.output_all_matches_filtered(:1029, used by find/search--all --dir):format!("{}/{}", dir_path, m.name)—..segments escape the requested directory.select_and_output_matches_filteredwith a directory (:1131): the same"{dir}/{name}"construction for interactively selected matches.select_and_output_matches_filteredto-file (:1142-1143):output_file.unwrap_or(&m.name)— name verbatim again.
- The write performs no containment. All five pass the path to
ReplContext::get(commands/repl.rs:550), which callsexport_blob(pkgs/id/src/lib.rs:547):to_absolute(lib.rs:358) prepends the cwd if relative, does not normalize.., and passes absolute paths through unchanged; iroh-blobs export then creates-or-truncates the target. Existing files are overwritten silently. - The fixed helper is out of reach.
download_output_path(the id(find/search): a remote node's match name becomes the download path verbatim — ../ and absolute names overwrite arbitrary files #1614 fix) is a private function ofcommands/find.rs; the REPL never calls it. Contrast inside the REPL itself:peekfetches via a temp file (runner.rs:1251) — the safe shape already exists in this surface too.
Verification (this run)
Verbatim-logic probe on the laptop (~/git/fleet/probe-2026-10-29-laneA-replpath/; the runner.rs constructions, lib.rs's to_absolute, and the CLI's download_output_path copied character-for-character from main 5a34fa70; writes use export's create-or-truncate semantics). Run 2026-10-29 ~12:16Z:
- Default file output, name = an absolute path: the pre-existing file at that path is overwritten with the blob bytes (
victim_now="ATTACKER-BYTES"). - Default file output, name
../victim-rel.txt: the file one level above the cwd is overwritten ("ATTACKER-BYTES"). --all --dir downloads, name../victim-dir.txt: writesdownloads/../victim-dir.txt— the file one level above the requested directory (work/victim-dir.txt) ends up containingATTACKER-BYTES; the write escaped the directory the user named.- Control, name
plain.txt: written insidedownloads/as intended. - Contrast — the CLI helper on the same names:
download_output_path(Some("downloads"), "../victim-dir.txt") = Ok("downloads/victim-dir.txt"), absolute name → final component only, and..//are refused with a named error.
Candidate verified by code-reading in the 2026-10-29 07:05 CDT creator run; the probe was prepared by that run and executed by its follow-up run after the original ended early.
Why it matters
The REPL is the interactive way to browse and pull files from a peer (find/search with --file or --all --dir are its documented download flows). The user names a query, not an output path; the match's name is invisible until after the write unless they list first. The overwrite target and content are both attacker-controlled in connected mode, bounded only by the user's filesystem permissions. The local variant matters too: any traversal name that lands in a store (#1621 — rename accepts one today) turns a later REPL find --file into the same escape.
Acceptance criteria
- A REPL find/search match name can never cause a write outside the location the user asked for, at all five sites (
runner.rs:769,:834-835,:1029,:1131,:1142-1143): match-derived default paths are confined to the cwd tree (default mode) or the requested directory (--dirmode), covering.., nesteda/../../b, absolute names, and names with no final component (refused with a named error, nothing written). - The REPL and the CLI share ONE implementation of the final-component ruling (e.g.
download_output_pathmoved somewhere both can call) — not a third copy of the rule that can drift again. - The
--all --dircollision behavior (two matches reducing to the same final component) is stated in the PR and matches the CLI behavior tracked in id(find/search): PR #1622's final-component confinement makes distinct --all --dir matches silently overwrite each other #2178, or is explicitly coordinated with it. - The probe cases above are pinned as tests at the path-construction level (no network needed); ordinary names (plain basenames, spaces, Unicode) download exactly as today in all modes, local and remote.
- Output paths the user names explicitly (
--file <path>,> path) remain the user's choice — only match-derived defaults are confined.
Activity
developing-today commentedon Oct 29, 2026
CODE-1160
dezren39 commentedon Oct 29, 2026
🤖 Agent note · filed
dezren39 commentedon Nov 1, 2026
🤖 Agent note · picked up
117 remaining items
dezren39 commentedon Nov 2, 2026
agent-note:v1
event: parked
task: 5238 (repo-health-code-pilot, repo-worker-code; OpenChamber scheduled lane)
state: Parked 20 minutes. Implementation is complete and checkpointed but uncommitted on branch queue/5238-2307-repl-download-paths in /home/user/git/fleet/5238-2307-repl-download-paths (from current origin/main): download_output_path moved to helpers.rs as the single shared ruling, commands/find.rs uses it, all five REPL match-derived sites in repl/runner.rs route through it with batch preflight refusing unsafe names and basename collisions before any write, plus three new regression tests. Targeted cargo tests could not complete:
cargo test -p id --lib repl::runner::tests::test_interactive_dirtimed out after 600s compiling under fleet load (41.60/50.68/60.78 at park). Labels show queue/parked + status/parked; work/assigned remains.resume: In a lower-load window, rerun the targeted tests on the preserved branch first, then commit, push, open the PR (noting the symlink-at-destination scope limit shared with CLI #1614), and merge on green.
dezren39 commentedon Nov 2, 2026
Queue task 5238 revalidated and parking: implementation preserved on branch queue/5238-2307-repl-download-paths (worktree ~/git/fleet/5238-2307-repl-download-paths), issue OPEN — but laptop load 120/125/105 on 16 cores still far above the <20 gate; cargo targeted tests cannot run reliably. Nothing changed. Resume in a low-load window: rerun targeted tests first on the preserved branch, then open PR.
dezren39 commentedon Nov 2, 2026
🤖 Agent note · picked up
fix(id): keep derived dash filenames off the stdout sentinel; dedupe …
Merge pull request #4472 from developing-today/queue/5238-2307-repl-d…
dezren39 commentedon Nov 2, 2026
🤖 Agent note · done
cargo test -p id --lib repl::runner::tests50 passed / 0 failed anddownload_output_pathtests 5 passed / 0 failed (laptop). Codex review: two P2 findings on the first head (derived-vs stdout sentinel; duplicate identical selections) were fixed in b61edc3 with regression tests and replied on their threads; the Codex re-review of b61edc3 completed with no findings.