🛡️ Sentinel: [HIGH] Fix authorization bypass in circuit breakers list - #87
Conversation
Bumps edition 2021 -> 2024 across all 11 crates (rust-version 1.93 already met the 1.85 MSRV). Full verification: clippy -D warnings, fmt --check, and cargo test --workspace (94 suites, 3825 tests) all clean on a from-scratch rebuild. Pre-migration: - cargo fix --edition applied mechanical idiom fixes (redundant `ref` bindings removed under 2024's match-ergonomics rules). Drop-order review: - Edition 2024 changes when temporaries with custom Drop impls get dropped inside if-let/while-let/match statements. Audited all 33 unique sites the migration tool flagged: bytes::Bytes refcounts, SecretString/SigningKey zeroing (earlier drop is safer, not riskier), CancellationToken/JoinHandle/tokio-internal timer drops, hashbrown/ moka internal cleanup, sqlx query results, object_store BoxStream. None had cross-statement dependencies or lock-release-order risk; no code changes needed. Mandatory-unsafe fix: - Edition 2024 requires unsafe blocks around std::env::set_var/ remove_var, conflicting with this workspace's unsafe_code = deny policy in 2 test files (orch8-engine, orch8-server; 9 call sites). Added serial_test to workspace.dependencies and tagged the affected tests #[serial(...)] to eliminate the actual env-var race, then added scoped #[allow(unsafe_code)] with SAFETY comments referencing the serialization. Idiom modernization: - Converted ~95 clippy::collapsible_if sites to if-let chains across orch8-engine, orch8-api, orch8-server, orch8-mobile, orch8-cli, orch8-grpc, and orch8. Sites with an else branch or a multi-statement body were left as nested ifs, since a chain can't express those. Also includes a workspace-wide cargo fmt normalization pass (import ordering) after a local rustfmt version drift surfaced on nearly every touched file.
…context field StepContext's doc comment claimed it was "cheaply cloneable," but that was never measured. Added a criterion benchmark (step_context_clone_* in engine_bench.rs) that clones StepContext at realistic ExecutionContext.data sizes: ~1us empty, ~113us at 10KB, ~596us at 50KB, and ~3.2ms at 256KB (the engine's own DEFAULT_MAX_CONTEXT_BYTES ceiling). agent.rs's tool-calling loop clones the context once per LLM call and once per tool call, so this scales with iteration count on top of the per-clone cost. Fix: wrap StepContext.context in Arc<ExecutionContext>. Audited every handler in the crate first to confirm none mutate .context directly (the scheduler is the sole writer, on its own owned copy) — read call sites are unaffected via Deref, and the two test-only sites that flip the dry_run flag now go through Arc::make_mut instead of a direct field assignment. Every construction site that used to clone/move a plain ExecutionContext now wraps it in Arc::new once at construction; every downstream StepContext::clone() (agent.rs, step_block.rs, step_exec.rs, and anywhere else that clones a context to build a "sub" context for a nested dispatch) is now an O(1) refcount bump instead of an O(context size) deep clone. Verified: cargo check/clippy -D warnings/fmt --check clean across the full workspace (StepContext's field is public but every consumer besides the handlers crate itself only reads through it).
Added the `OptionalAdmin` extractor and explicit `is_some()` check to the `list_all_breakers` handler. Previously, an unscoped API key without a tenant header could theoretically access the fallback `registry.list_all()` if it was present, or simply hit an unhandled path. Now it securely requires an admin context to return cross-tenant state, properly guarding against state leakage. Co-authored-by: ovasylenko <3797513+ovasylenko@users.noreply.github.com>
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
🚨 Severity: HIGH
💡 Vulnerability: The
/circuit-breakersendpoint returned an empty list for callers scoped to a tenant without an admin context instead of returning an admin list, but the endpoint lacked an explicit admin check on its unscoped branch. An unscoped API caller could list all circuit breakers for all tenants, leaking cross-tenant state.🎯 Impact: An attacker or generic API key holder without admin privileges but with an unscoped key could access the cross-tenant circuit breaker list, potentially learning about other tenants' operational issues and handler states.
🔧 Fix: Added the
admin_ctx: crate::auth::OptionalAdminextractor and updated the logic to only returnregistry.list_all()ifadmin_ctx.is_some().✅ Verification: Ran
cargo clippy -p orch8-api -- -A unknown_lintsandcargo test -p orch8-api --tests.PR created automatically by Jules for task 16656816719653836942 started by @ovasylenko