Skip to content

id(find/search): a remote node's match name becomes the download path verbatim — ../ and absolute names overwrite arbitrary files #1614

Closed

Description

@dezren39

Summary

id find (and id search) use the name of a find match verbatim as the download path. When the search targets a remote node, that name comes straight from the node's MetaResponse::Find — postcard-decoded with no validation — and the same node also supplies the blob content that gets written there. A malicious or compromised node can therefore make a client's id find <NODE> <query> overwrite arbitrary files the user can write: a match named /home/user/.bashrc (single-match mode uses the name as the whole output path), or ../victim.txt (both single-match and --all --dir modes), is written with attacker-chosen content, truncating whatever was there.

Same defect family as CLOSED #1595 (a file name reaching a filesystem path with no working containment check), in the download path instead of the compile staging path.

The chain (all on main 63ba1705)

  1. Names arrive unchecked. cmd_find_matches deserializes MetaResponse::Find { matches } from the remote node and returns them as-is (pkgs/id/src/commands/find.rs:901-913). FindMatch.name is a plain String (pkgs/id/src/protocol.rs:131-143). A hostile server need not run this codebase at all — the response is just postcard.
  2. Names become paths, four sites:
    • cmd_find single/first-match: let output = if to_stdout { "-" } else { &m.name } (find.rs:268 and :280) — the name IS the path, so absolute names land anywhere.
    • cmd_find --all --dir: let output_path = format!("{}/{}", dir_path, m.name) (find.rs:237) — .. segments escape the requested directory.
    • cmd_search --all --dir: the same construction (find.rs:369).
      All four pass the path to cmd_get_one_remote / cmd_get_one, which fetch the blob (from the same remote node in the remote case) and export it.
  3. The write performs no containment. export_blob (pkgs/id/src/lib.rs:546-563) passes the path through to_absolute (lib.rs:357-363 — prepends the cwd if relative, no normalization of .., absolute paths pass through unchanged) into iroh-blobs Store::export (ExportMode::Copy), which creates-or-truncates the target (File::create / reflink-or-copy, iroh-blobs 0.103 src/store/fs.rs:1272,1277). Existing files are overwritten silently.
  4. Honest nodes can hold such names too. Nothing stops a served store from containing a traversal name: blob rename validation (fileops.rs:141-151 validate_target) checks only that the target is non-empty and different — ../../x is an acceptable rename target today. So the exposure is not limited to hand-crafted protocol responses.

Contrast: cmd_peek fetches the same matches into a NamedTempFile instead (find.rs:620-631) — the safe shape already exists in this file.

Verification (this run)

Verbatim-logic probe on the laptop (~/git/fleet/probe-2026-10-28-laneA-findpath; find.rs sha256 5c6cb299…, lib.rs af81794b… — identical in the sandbox and laptop clones; probe + log in goal bookkeeping hidden_files/probe-2026-10-28-laneA-findpath/, confirmed by two independent runs). The probe copies the find.rs:237 and find.rs:268 path constructions and lib.rs's to_absolute character-for-character, and writes with export's create-or-truncate semantics:

  • --dir mode, name ../victim.txt: writes downloads/../victim.txt — a pre-existing file outside the download dir is overwritten with the blob bytes.
  • Single-match mode, name = an absolute path: the pre-existing file at that path is overwritten.
  • Single-match mode, name ../victim2.txt from a subdirectory cwd: the file one level up is overwritten.
  • Control, name plain.txt: written inside the download dir as intended.

Why it matters

id find <NODE> query is the documented way to pull a file from a peer ("Find and output first match", find.rs doc examples) — the victim does nothing unusual, names no output file, and may not even know the match's name is hostile, because find matches by query against the server's store. The overwrite target and content are both attacker-controlled, bounded only by the victim's filesystem permissions (dotfiles, SSH keys, project files are all in reach). The local-store variant also matters: any flow that lands a traversal name in a store (rename accepts one today) turns a later local find into the same escape.

Acceptance criteria

  • A find/search match name can never cause a write outside the location the user asked for: in single/first-match mode the effective path is confined to the current directory tree the user invoked from (or the match is refused with a named error), and in --all --dir mode every write stays inside the given directory after path normalization — covering .., nested a/../../b, and absolute names at all four sites (find.rs:237, :268, :280, :369).
  • Existing files outside the permitted location are never truncated by a find/search download; the probe cases above are pinned as tests (unit tests at the path-construction/sanitizing helper are enough — no network needed).
  • The ruling for names that are not a single safe segment is stated in the PR: refuse, take the final component, or sanitize — including what happens to legitimate names containing / if any supported flow creates them (rename currently accepts them; if the ruling breaks those, that consequence is called out and rename-side validation is addressed or filed separately).
  • Ordinary names (plain basenames, spaces, Unicode) download exactly as today in all modes, local and remote.

Activity

  1. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  2. added
    kind/securitySecurity issue or security hardening
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/highImportant and time-sensitive
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Oct 28, 2026
  3. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (lane A, new-issue-creator-code-a)
    • Session / ID: new-issue-creator-code-a run 2026-10-28 02:05 CDT
    • Found via: filed this run
    • Task: Filed after verifying on main 63ba170 with a verbatim-logic probe on the laptop (two independent runs): cmd_find/cmd_search use a find match's name verbatim as the download path at four sites (find.rs:237, :268, :280, :369); in remote mode the name and the blob both come from the queried node (MetaResponse::Find is postcard-decoded unchecked), and export_blob/to_absolute + iroh-blobs export create-or-truncate with no containment — ../ and absolute names overwrote pre-existing files outside the requested location in every probe case, control stayed inside. Honest stores can also hold traversal names (fileops validate_target accepts them).
    • Expectation: effort/s — triage pins the ruling for non-segment names (refuse vs final-component vs sanitize, incl. the rename-side consequence), then a fix with the probe cases pinned as unit tests at the path-construction helper.
    • Links: probe + log in goal bookkeeping (hidden_files/probe-2026-10-28-laneA-findpath/); queue packet enqueued to repo-health-code-pilot (triage). Sibling of CLOSED id(world-compile): sanitize_file_name's containment check can never fire — split('.') parts can never equal "." or "..", so the staging guard enforces nothing #1595 (same missing-containment family, compile staging).
  4. added
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 28, 2026
  5. 51 remaining items

  6. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up (resume, awaiting CI — 1 check left)

    • Agent: queue worker (repo-worker-code), Lane A shift
    • Session / ID: queue task 1738 (resume), claimed 2026-10-28T11:06Z
    • Found via: queue claim (resume per §10)

    Resuming at awaiting_ci: PR #1622 (head b5e56e28, unchanged) OPEN/MERGEABLE with 32 SUCCESS / 0 failures — only the aggregate nix flake check status remains QUEUED. Polling inside the 5-minute window: merge on green and complete as fixed_merged; otherwise re-park.

  7. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked — one aggregate check still queued

    • Agent: queue worker (repo-worker-code), Lane A shift
    • Session / ID: queue task 1738, parked 2026-10-28T11:12Z
    • Found via: queue claim (resume)

    PR #1622 (head b5e56e28) at park: 32 SUCCESS / 0 failures, MERGEABLE — every sub-check is green; only the aggregate nix flake check status is still QUEUED (unchanged across this shift's 5-minute poll; same runner-queue stall affecting the fleet today). Not merging against a non-green aggregate. Resume condition: aggregate flips SUCCESS → merge PR #1622 → complete task 1738 as fixed_merged (issue closes via the PR).

  8. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Oct 28, 2026
  9. added a commit that references this issue on Oct 28, 2026
  10. removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    work/assignedQueue worker lifecycle state
    on Oct 28, 2026
  11. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    Queue task 1738 done (Lane B): PR #1622 merged (merge sha 1980275) with all 33 checks SUCCESS on head b5e56e2. This shift re-reviewed the diff fresh: all four find/search download write sites route through download_output_path (final component only; names with no final component refused, nothing written), ordinary names byte-identical, unit tests pin traversal/absolute/refusal cases. Issue auto-closed by the merge.

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 jobstatus:donetask/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