8.6 KiB
Independent review — Tier-3 safety batch
Verdict: APPROVE WITH NITS
Scope reviewed: git diff main...fix/audit-2026-06-11-tier3 (8 commits), every touched
source file traced end-to-end, targeted non-DB unit tests run green, clippy clean on the
four touched Rust crates. DB-backed #[ignore] integration tests were read but not run.
Per-fix verdict
1. PrincipalCache revocation-lag — PASS
Traced the Arc end to end. crates/picloud/src/lib.rs:503 constructs ONE
Arc<PrincipalCache> and .clone()s it into AuthState (510), AdminsState (523),
and ApiKeysState (537). The middleware reads via resolve_principal →
state.principal_cache.get(...) (auth_middleware.rs:242) where state is the same
auth_state wired into both require_authenticated (lib.rs:571-573) and auth_router
(lib.rs:596). It is genuinely one shared instance, so an evict on the revocation side is
visible to the resolve side. No second AuthState is constructed anywhere.
Keying is correct: evict_user(user_id) (retain(p.user_id != user_id)) on deactivation
(admin_users_api.rs:339), password change (admin_users_api.rs:266), and single
API-key delete (api_keys_api.rs:170 — coarse-by-design because the cache is hashed by
token, not key id); evict_token(&hash) (precise, single session) on logout
(auth_api.rs:201). Unit tests evict_user_drops_only_that_users_entries and
evict_token_drops_only_that_entry pass and actually assert the targeting (Bob's entry
survives; the user's other session survives logout).
Note: this remains a single-process cache; in cluster mode (deferred) revocation would
still lag on peer nodes. Acceptable for MVP/single-node.
2. content-type sanitizer — PASS
shared/src/files.rs:280 — if ct.bytes().any(|b| b < 0x20 || b == 0x7f) { return SAFE_RENDER_FALLBACK } runs FIRST, before the split(';') / allowlist logic. Byte check
is exactly < 0x20 || == 0x7f. Test control_chars_are_coerced_even_under_an_allowlisted _prefix covers image/png\r\n…, bare \n, \u{7f}, \u{0} and passes. CRLF in an
allowlisted prefix now coerces to application/octet-stream. Closes the header-injection
/ HeaderValue-panic vector.
3. disable debug — PASS
engine.disable_symbol("debug") sits next to disable_symbol("print") inside
build_engine (executor-core/src/engine.rs:412), which is the per-call builder invoked
on every execution path (incl. the real run path at engine.rs:283). Test
debug_and_print_symbols_are_disabled asserts both print("x") and debug("x") fail as
ExecError::Parse; passes.
4. runtime-error scrub — PASS
The scrub is in orchestrator-core/src/api.rs:753 inside impl IntoResponse for ApiError
— the public data-plane response path (/api/v1/execute/{id} + user-route fallback). It
logs full detail under a correlation_id (warn) and returns "script execution error (ref: <uuid>)". I checked there is NO admin/authenticated path reusing this IntoResponse
— the only other ExecError consumer in manager-core is dispatcher.rs (queue/cron
background, stores last_error internally, not a public HTTP body). Parse /
InvalidResponse / Timeout / OperationBudgetExceeded stay verbose. Parse can echo
script-syntax detail, but that is the caller's own script and is deterministic/compile-
shape, not internal infra detail — acceptable. The handback's claim of a separate "verbose
admin script-test path" appears moot (no such endpoint found), but that is harmless — the
public path is the one that mattered and it is scrubbed.
5. files collection validation — PASS
All four repo READ/DELETE methods now guard at method entry: head
(files_repo.rs:353), get (373), delete (439), list (485); create/update (321,397)
already guarded. guard_collection rejects empty / / / \ / .. / \0. The three
admin endpoints call validate_files_collection (a re-export of shared::validate_ collection, same reject-set) BEFORE touching the repo — list_files (files_api.rs:91),
get_file (153), delete_file (198) — yielding a clean 422 instead of a repo-surfaced
500. Belt-and-suspenders is real, both layers verified.
6. body limits — PASS
DefaultBodyLimit::max(1 MiB) is applied with .layer(...) on the email-inbound Router
BEFORE .with_state (email_inbound_api.rs:184-186) — correct axum ordering, layer wraps
the route. Caddy request_body { max_size 12MB } is placed inside the site block in both
caddy/Caddyfile and caddy/Caddyfile.prod (valid placement). 12 MB (Caddy decimal =
12,000,000) ≥ the orchestrator's 10 MiB user-route read (10,485,760, api.rs:219), so
legit large invokes pass while the multi-GB Caddy default is replaced. Admin/execute keep
axum's default extractor limit.
7. CLI argv-secret rejection — PASS
resolve_password (admins.rs:51) rejects Some(non-dash) with a clear error, accepts
only Some("-") (stdin) and errors on None. login.rs:30 rejects inline --token,
accepts --token - (stdin) and still honors PICLOUD_TOKEN env. Interactive prompts use
rpassword::prompt_password (true no-echo) for both password and token; the old echoing
read_line trait was removed. rpassword = "7" is a declared dep. No internal caller
passes an inline secret: pic secrets set already reads value from stdin only
(secrets.rs), and the only resolve_password callers are create/set. New test
create_with_inline_password_is_rejected asserts --password hunter2 fails with "inline
is not allowed".
8. X-Forwarded-For last-hop — PASS
extract_remote_ip now uses s.split(',').next_back() (auth_api.rs:268), trims, falls
back to "unknown" on absent/empty/whitespace header (verified: xff(" ") → trimmed
empty → falls through to "unknown"). Trust model is correct for the documented single-
Caddy topology: Caddy's reverse_proxy APPENDS the real peer as the last XFF hop, so earlier
entries are client-spoofable and the last entry is authoritative. Three unit tests (spoof-
stable, single-hop, missing/empty) pass. The doc-comment honestly flags the multi-proxy
caveat as deferred to cluster work.
Bootstrap-sentinel deferral — VALID
Verified BOTH guards exist in admin_users_api.rs:
- Deactivation path:
patch_adminat 298-302 returnsLastActiveAdminwhencount_active_excluding(id) == 0(plus a last-owner guard at 310-314). - Delete path:
delete_adminat 372-385 mirrors it (count_active_excluding == 0→LastActiveAdmin, plus last-owner guard). The delete guard only firesif target.is_active, but an inactive admin is not counted bycount_active, so deleting one cannot drivecount_active()to 0. There is no API path tocount_active() == 0; reaching it requires direct SQL (= DB-write compromise, at which point re-seed is moot). The deferral rationale holds. Sentinel needs a durable marker (schema migration) which is out of the chosen no-schema scope — reasonable to defer.
New issues / regressions found
None blocking. Nits:
- (nit, stale doc)
auth_middleware.rs:141-142still says theprincipal_cachefield is "Nonefor tests / harnesses that don't wire it", but the field is a non-OptionArc<PrincipalCache>. Cosmetic leftover; update or drop the sentence. - (nit)
crates/picloud-cli/tests/admins.rscreate_with_inline_password_is_rejectedis gated#[ignore = "needs DATABASE_URL"], but the inline rejection happens inresolve_passwordbefore any network/DB call, so the test would pass without Postgres. Harmless over-gating; could be un-ignored for cheap coverage. - (observation, out of scope) The API-key-delete eviction is coarse (
evict_user, dropping all of that user's cached sessions+keys) because the cache is keyed by token hash and the key id can't derive it. Documented and acceptable; a per-key revocation would need a secondary index. Not a defect.
Tests adequacy
Adequate. Each fix has a test that exercises behaviour, not just compilation:
content-type coercion under allowlisted prefix; both print+debug rejection; XFF last-
hop incl. spoof-stability; evict_user/evict_token targeting; inline-password rejection;
and a DB-backed runtime_error_detail_is_scrubbed_from_public_response that throws a marker
string and asserts it never appears in the public body while a ref: id does. The four
non-DB suites I ran are green (5 + 1 + 9 + 3). DB-backed tests read as correct but unrun.
Bottom line
All 8 fixes correctly close their stated gaps with no regression or new vulnerability found. The critical shared-Arc invariant for fix #1 is genuinely satisfied. The bootstrap-sentinel deferral rationale is verified accurate. Only three cosmetic nits, none blocking. Approve and ff-merge.