Repository navigation
Description
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)
- Names arrive unchecked.
cmd_find_matchesdeserializesMetaResponse::Find { matches }from the remote node and returns them as-is (pkgs/id/src/commands/find.rs:901-913).FindMatch.nameis a plainString(pkgs/id/src/protocol.rs:131-143). A hostile server need not run this codebase at all — the response is just postcard. - Names become paths, four sites:
cmd_findsingle/first-match:let output = if to_stdout { "-" } else { &m.name }(find.rs:268and: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 tocmd_get_one_remote/cmd_get_one, which fetch the blob (from the same remote node in the remote case) and export it.
- The write performs no containment.
export_blob(pkgs/id/src/lib.rs:546-563) passes the path throughto_absolute(lib.rs:357-363— prepends the cwd if relative, no normalization of.., absolute paths pass through unchanged) into iroh-blobsStore::export(ExportMode::Copy), which creates-or-truncates the target (File::create/ reflink-or-copy, iroh-blobs 0.103src/store/fs.rs:1272,1277). Existing files are overwritten silently. - Honest nodes can hold such names too. Nothing stops a served store from containing a traversal name: blob rename validation (
fileops.rs:141-151validate_target) checks only that the target is non-empty and different —../../xis 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:
--dirmode, name../victim.txt: writesdownloads/../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.txtfrom 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 --dirmode every write stays inside the given directory after path normalization — covering.., nesteda/../../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
developing-today commentedon Oct 28, 2026
CODE-638
dezren39 commentedon Oct 28, 2026
🤖 Agent note · filed
51 remaining items
dezren39 commentedon Oct 28, 2026
🤖 Agent note · picked up (resume, awaiting CI — 1 check left)
Resuming at
awaiting_ci: PR #1622 (headb5e56e28, unchanged) OPEN/MERGEABLE with 32 SUCCESS / 0 failures — only the aggregatenix flake checkstatus remains QUEUED. Polling inside the 5-minute window: merge on green and complete as fixed_merged; otherwise re-park.dezren39 commentedon Oct 28, 2026
🤖 Agent note · parked — one aggregate check still queued
PR #1622 (head
b5e56e28) at park: 32 SUCCESS / 0 failures, MERGEABLE — every sub-check is green; only the aggregatenix flake checkstatus 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).Merge pull request #1622 from developing-today/queue/1738-1614-find-d…
dezren39 commentedon Oct 28, 2026
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.