Repository navigation
Description
Problem
sanitize_file_name in pkgs/id/src/world_compile.rs:505-511 is the guard that is supposed to keep a compile request's file names inside the per-compile job directory at the staging write (world_compile.rs:222, job.path().join(sanitize_file_name(name)?) followed by tokio::fs::write). Its check can never fire:
fn sanitize_file_name(name: &str) -> Result<String> {
ensure!(
name.split('.').all(|part| part != "." && part != ".."),
"file name {name:?} must stay inside the job directory"
);
Ok(name.to_owned())
}split('.') yields maximal runs of non-dot characters, so no part can ever equal "." or ".." (both contain a dot). Every input passes, including "..", "../evil.roc", and "/etc/passwd". The function returns every name unchanged and has no test.
Containment today rests entirely on a different layer: validate_spec (world_compile.rs:157-168) refuses any name outside [A-Za-z0-9._-] or starting with ., which incidentally excludes /. That is the only thing standing between a peer-supplied compile file name and a write outside the job tempdir — the dedicated guard at the write site enforces nothing, and its error message claims a guarantee it does not provide. Any future loosening of the validate_spec charset (e.g. allowing subdirectory module names like modules/Foo.roc, a natural Roc layout) silently turns this into a path-escape write of attacker-chosen content, with no failing test and no error at the write site.
Evidence
Verified on main 65165193 with a probe on the laptop (~/git/fleet/probe-2026-10-28-laneA-sanitize there) evaluating the verbatim check expression from world_compile.rs:505-511 (file sha256 e9bb8327eb5647e3c40e7611cebb335660f1a127392085cb00622378df83f11f; ensure! expanded by hand, plus a lexical Path::components normalization to test whether job.join(name) escapes). Log: hidden_files/probe-2026-10-28-laneA-sanitize/out.log in the goal workspace.
"main.roc": sanitize_passes=true escapes_job=false
"Helper.roc": sanitize_passes=true escapes_job=false
"..": sanitize_passes=true escapes_job=true
"../evil.roc": sanitize_passes=true escapes_job=true
"../../evil.roc": sanitize_passes=true escapes_job=true
"a/../../evil.roc": sanitize_passes=true escapes_job=true
"sub/../..//evil.roc": sanitize_passes=true escapes_job=true
"/etc/passwd": sanitize_passes=true escapes_job=true
"a..b.roc": sanitize_passes=true escapes_job=false
split('.') of "...." = ["", "", "", "", ""]
Controls: legitimate flat names (main.roc, Helper.roc, a..b.roc) pass and stay inside the job dir. The existing test at world_compile.rs:599 (validate_spec refuses "../evil.roc") passes only because of the charset allowlist — it never exercises sanitize_file_name.
Why it matters
- Defense in depth that isn't: the one function whose job is containment at the write site is dead code as a check, so a reviewer reading the staging loop reasonably believes traversal is handled there.
- The failure mode on any future charset loosening is silent: peer-supplied file content written outside the job tempdir (compile requests arrive from world-session clients;
world_session/mod.rs:1205-1212passes wire files straight intoCompileSpec), instead of a loud refusal. - Sibling of id(world-compile): duplicate file names in a compile request are never refused — the later file silently overwrites the earlier, and source_blake3 provenance describes sources that were never compiled #1593 (duplicate names, fixed): that fix hardened
validate_spec; this is the remaining unverified guard in the same staging path.
Acceptance criteria
sanitize_file_nameactually enforces its contract: it refuses any name whosePath::componentsare not exactly oneComponent::Normal(refusingParentDir,RootDir, prefixes, separators, and empty names), or an equivalently sound component-based check.- Unit tests in
world_compile.rs's test module callsanitize_file_namedirectly:"..","../evil.roc","a/../../evil.roc","/etc/passwd"are refused;"main.roc","Helper.roc","a..b.roc"pass unchanged. - No behavior change for any name
validate_speccurrently accepts (flat[A-Za-z0-9._-]names not starting with.). - Existing
world_compiletests still pass.
Activity
developing-today commentedon Oct 28, 2026
CODE-624
dezren39 commentedon Oct 28, 2026
🤖 Agent note · picked up
dezren39 commentedon Oct 28, 2026
🤖 Agent note · done
cargo test --lib world_compile22 passed / 0 failed incl. the new sanitize_file_name_refuses_traversal; mutation check: the new test fails against the original implementation; rustfmt clean. PR CI was still pending at merge; merged under the standing directive on local verification.