Skip to content

id(repl): find/search download paths use the match name verbatim — the #1614 confinement never reached the REPL copies (../ and absolute names overwrite arbitrary files) #2307

Closed

Description

@dezren39

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)

  1. Names arrive unchecked. In connected mode, ReplContext::find (pkgs/id/src/commands/repl.rs, find at ~:696) returns the remote node's MetaResponse::Find matches postcard-decoded as-is; FindMatch.name is a plain String. 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).
  2. Names become paths, five sites, all in pkgs/id/src/repl/runner.rs:
    • handle_find_command single match (:769): args.output_file.unwrap_or(&m.name) — with --file and no explicit output name, the match name IS the path; absolute names land anywhere.
    • handle_search_command first 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_filtered with a directory (:1131): the same "{dir}/{name}" construction for interactively selected matches.
    • select_and_output_matches_filtered to-file (:1142-1143): output_file.unwrap_or(&m.name) — name verbatim again.
  3. The write performs no containment. All five pass the path to ReplContext::get (commands/repl.rs:550), which calls export_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.
  4. 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 of commands/find.rs; the REPL never calls it. Contrast inside the REPL itself: peek fetches 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: writes downloads/../victim-dir.txt — the file one level above the requested directory (work/victim-dir.txt) ends up containing ATTACKER-BYTES; the write escaped the directory the user named.
  • Control, name plain.txt: written inside downloads/ 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 (--dir mode), covering .., nested a/../../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_path moved somewhere both can call) — not a third copy of the rule that can drift again.
  • The --all --dir collision 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

  1. added
    kind/securitySecurity issue or security hardening
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    priority/highImportant and time-sensitive
    on Oct 29, 2026
  2. developing-today commented on Oct 29, 2026

    @developing-today
    Contributor
  3. added
    queue/queuedHas a queued task in the Supabase agent queue
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Oct 29, 2026
  4. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  5. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

    • Agent: OpenChamber scheduled lane (repo-worker-code)
    • Task: queue task 3458; verify current REPL path handling and route the appropriate follow-up.
  6. added
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  7. 117 remaining items

  8. added
    status/parkedDeliberately paused with a recorded resume condition
    on Nov 2, 2026
  9. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    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_dir timed 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.

  10. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    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.

  11. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 2, 2026
  12. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  13. added 2 commits that reference this issue on Nov 2, 2026
  14. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)effort/sSmall: expected under 2 hours of workkind/securitySecurity issue or security hardeningpriority/highImportant and time-sensitivesource/creatorFiled by the new-issue-creator jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions