Skip to content

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

Closed

Description

@dezren39

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

Acceptance criteria

  • sanitize_file_name actually enforces its contract: it refuses any name whose Path::components are not exactly one Component::Normal (refusing ParentDir, RootDir, prefixes, separators, and empty names), or an equivalently sound component-based check.
  • Unit tests in world_compile.rs's test module call sanitize_file_name directly: "..", "../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_spec currently accepts (flat [A-Za-z0-9._-] names not starting with .).
  • Existing world_compile tests still pass.

Activity

  1. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  2. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

    • Agent: new-issue-creator-code-a (Lane A, new-issue-creator-code-a cron)
    • Session / ID: new-issue-creator-code-a run 2026-10-28 00:05 CDT
    • Found via: filed this run; self-assigned under the guarded trivial self-solve (§4 of the lifecycle protocol)
    • Task: replace sanitize_file_name's vacuous split('.') check with a Path::components check (exactly one Normal component) and add direct unit tests for traversal names
    • Expectation: effort/xs; done = fix PR merged with world_compile tests green on the laptop/CI, issue closed
    • Links: branch creator-a/1595-sanitize-containment (laptop worktree)
  3. added
    kind/securitySecurity issue or security hardening
    source/creatorFiled by the new-issue-creator job
    priority/lowNice to have; do when convenient
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/xsTrivial: expected under 30 minutes of work
    work/self-assignedAn agent grabbed this item directly, outside the queue
    on Oct 28, 2026
  4. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

    • Agent: new-issue-creator-code-a (Lane A, new-issue-creator-code-a cron)
    • Session / ID: new-issue-creator-code-a run 2026-10-28 00:05 CDT
    • Found via: filed this run; self-assigned under the guarded trivial self-solve (§4 of the lifecycle protocol)
    • Task: component-based containment check in sanitize_file_name + direct unit tests — complete
    • Expectation: done = PR merged with tests green. PR fix(id): make sanitize_file_name enforce job-directory containment #1597 merged (merge a9c8540, fix fffe6d8); laptop real-crate cargo test --lib world_compile 22 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.
    • Links: PR fix(id): make sanitize_file_name enforce job-directory containment #1597
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/xsTrivial: expected under 30 minutes of workkind/securitySecurity issue or security hardeningpriority/lowNice to have; do when convenientsource/creatorFiled by the new-issue-creator jobstatus:done

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions