Tasks: Untrusted-Path Containment Hardening

Mission: untrusted-path-containment-hardening-01KVFTFV | Branch: automation/sonar-security-20260619 (rides PR #2036) Spec: spec.md | Plan: plan.md

Tasks translate the plan's Implementation Concern Map (IC-01…IC-05, IC-00 baseline) into work packages, linearized per the plan's decomposition note to avoid ownership overlap on the shared status/ surface: WP01 audit (read-only) → WP02 (status/+core seam fixes) & WP03 (other-package fixes) → WP04 guard; WP05 (loopback) runs in parallel.

Subtask Index

IDDescriptionWPParallel
T001Define + commit the reproducible audit ruleset (seed-set + sink predicate)WP01
T002Run the audit; enumerate every untrusted→FS sink in src/specify_cliWP01
T003Classify each sink: routed-through-seam / unreachable / trusted-sourceWP01
T004Emit the audit record; assert emitted count == inventory rows (completeness)WP01
T005Document aggregate.py raise-guard + disposition its composed-path reads (FR-003)WP01
T006store.py _SlugResolver.resolve resolve()-containment via ensure_within_anyWP02
T007store.py symlink-escape negative + symlinked-root positive tests (mutation-verified)WP02
T008mission_metadata.resolve_mission_identity safe_mission_slug chokepoint (FR-009)WP02
T009Negative test: hostile meta.json + empty event slug → no write outside derived/ (views+lifecycle)WP02
T010Route any WP01-flagged reachable status/ sink through the seamWP02
T011Gates: ruff/mypy/tests; confirm #2036 baseline not regressed (FR-007)WP02
T012Disposition/fix events/decision_log.py mission_slug write sinkWP03[P]
T013Disposition/fix coordination/surface_resolver.py + missions/_read_path_resolver.py composed pathsWP03[P]
T014Disposition/fix dossier/drift_detector.py + migration/mission_state.pyWP03[P]
T015Disposition/fix review/arbiter.py + post_merge/review_artifact_consistency.py wp_id sinks; document review/cycle.pyWP03[P]
T016Negative tests for each confirmed-reachable WP03 fixWP03
T017Gates for WP03 changesWP03
T018Implement tests/architectural/ guard reading the WP01 inventory; assert audited surfaces use the seamWP04
T019Guard self-test: fixture join makes guard FAIL; removing guard makes fixture PASS (load-bearing)WP04
T020Confirm guard runs in the architectural gate; full suite greenWP04
T021Add loopback-only rationale docstring/comments to core/loopback_http.pyWP05[P]
T022Retain/strengthen tests/core/test_loopback_http.py 127.0.0.1-binding regression testsWP05[P]
T023Record the 2 Sonar hotspots (rule key + PR #2036) for UI reviewWP05[P]
T024Gates for WP05WP05

Work Packages

WP01 — Reproducible untrusted→FS sink audit (read-only inventory)

  • Goal: Produce a re-runnable audit (recorded ruleset) enumerating every untrusted-segment→FS sink in src/specify_cli, each with one disposition (routed-through-seam / unreachable / trusted-source). No production-code changes. (IC-02; FR-004, FR-003)
  • Priority: P1 (gates WP02/WP03/WP04). Independent test: re-running the committed ruleset reproduces the same inventory; every row has a disposition; emitted count == rows.
  • Subtasks:
  • ✅ T001 Define + commit the reproducible audit ruleset (WP01)
  • ✅ T002 Run the audit; enumerate every untrusted→FS sink (WP01)
  • ✅ T003 Classify each sink with a disposition + rationale (WP01)
  • ✅ T004 Emit the audit record; assert count completeness (WP01)
  • ✅ T005 Document aggregate.py raise-guard + disposition composed-path reads (WP01)
  • Dependencies: none. Est. size: ~300 lines.
  • Prompt: tasks/WP01-untrusted-sink-audit.md

WP02 — status/ + meta.json seam fixes (IC-01 + IC-05)

  • Goal: Add resolve()-containment to store.py resolver (FR-002) and route the meta.json-derived slug through safe_mission_slug at the single mission_metadata chokepoint (FR-009), closing the live write-path bypass in views.py/lifecycle.py; route any WP01-flagged reachable status/ sink. Mutation-verified tests incl. the macOS symlinked-root positive case. (FR-002, FR-007, FR-008, FR-009, C-004)
  • Priority: P1 (core fix). Independent test: hostile event-slug AND hostile meta.json-slug both fail closed (no read/write outside trusted roots); legitimate slug under a symlinked root accepted.
  • Subtasks:
  • ✅ T006 store.py resolve()-containment (WP02)
  • ✅ T007 store.py symlink-escape + symlinked-root tests (WP02)
  • ✅ T008 mission_metadata safe_mission_slug chokepoint (WP02)
  • ✅ T009 hostile meta.json + empty event slug negative test (WP02)
  • ✅ T010 route WP01-flagged reachable status/ sinks (WP02)
  • ✅ T011 gates + no-regression of #2036 baseline (WP02)
  • Dependencies: WP01. Est. size: ~450 lines.
  • Prompt: tasks/WP02-status-meta-seam-fixes.md

WP03 — Other-package reachable sink fixes (audit-driven)

  • Goal: For each WP01-confirmed-reachable sink outside status/ (the pre-named candidates + any the ruleset surfaces), route it through the canonical seam; for unreachable/trusted ones, record the disposition. Negative tests for each fix. (FR-001, FR-003, FR-004, FR-008)
  • Priority: P2. Independent test: each fixed sink rejects/falls-back on a traversal segment (negative test); unreachable sinks have a documented rationale.
  • Subtasks:
  • ✅ T012 events/decision_log.py (WP03)
  • ✅ T013 coordination/surface_resolver.py + missions/_read_path_resolver.py (WP03)
  • ✅ T014 dossier/drift_detector.py + migration/mission_state.py (WP03)
  • ✅ T015 review/arbiter.py + post_merge wp_id sinks; document review/cycle.py (WP03)
  • ✅ T016 negative tests for confirmed-reachable fixes (WP03)
  • ✅ T017 gates (WP03)
  • Dependencies: WP01. Est. size: ~480 lines.
  • Prompt: tasks/WP03-other-package-sink-fixes.md

WP04 — Load-bearing architectural regression guard (IC-03)

  • Goal: A tests/architectural/ guard, anchored on the WP01 inventory, that fails when a new unvalidated untrusted-segment join appears on an audited surface; proven load-bearing by a self-test. (FR-005, SC-006)
  • Priority: P2 (after fixes land). Independent test: a fixture join makes the guard fail; removing the guard makes that fixture test pass.
  • Subtasks:
  • ✅ T018 implement the guard reading the inventory (WP04)
  • ✅ T019 guard self-test (load-bearing) (WP04)
  • ✅ T020 confirm gate placement; full suite green (WP04)
  • Dependencies: WP01, WP02, WP03. Est. size: ~280 lines.
  • Prompt: tasks/WP04-architectural-regression-guard.md

WP05 — loopback_http.py rationale + hotspot record (IC-04)

  • Goal: Document the loopback-only (127.0.0.1) rationale in-code, retain the binding regression tests, and record the two Sonar hotspots for UI review. No behavioural change; no HTTPS forcing. (FR-006, C-001)
  • Priority: P3 (independent, parallel). Independent test: regression tests still assert 127.0.0.1 binding; rationale present; hotspot record cites rule key + PR #2036.
  • Subtasks:
  • ✅ T021 loopback rationale docstring/comments (WP05)
  • ✅ T022 retain/strengthen binding regression tests (WP05)
  • ✅ T023 record the 2 Sonar hotspots (WP05)
  • ✅ T024 gates (WP05)
  • Dependencies: none (parallel). Est. size: ~220 lines.
  • Prompt: tasks/WP05-loopback-rationale-hotspot.md

Dependency Graph

WP01 (audit) ──┬──▶ WP02 (status/meta fixes) ──┐
               └──▶ WP03 (other-pkg fixes)  ────┼──▶ WP04 (guard)
WP05 (loopback) ───────────────────────────────┘ (independent, parallel)

MVP / Sequencing

  • MVP: WP01 → WP02 (closes the highest-severity live write-path traversal via FR-009 + the store.py read residual).
  • WP03 broadens coverage to the rest of the CLI; WP04 locks it against regression; WP05 is independent documentation hardening.