Summary
Cross-context calls (#[app::xcall]) execute the target method under members.first() of the target context — a real owned/admin member with that context's full write authority. We should harden this to run xcalls under a least-privilege scoped identity, so an admitted xcall cannot exercise more authority in the target than the call actually needs.
Current state
crates/context/src/handlers/execute/mod.rs:1217:
// Find an owned member of the target context to execute as — one that has permissions there.
let members: Vec<_> = xcall_context_client
.get_context_members(&target_context_id, Some(true))
.try_collect()
.await
.unwrap_or_default();
let Some((target_executor, _is_owned)) = members.first() else { ... };
The target then executes via execute_with_origin(...) under target_executor's identity, with the source context tagged as env::xcall_origin().
What already mitigates this (why this is a follow-up, not a live hole)
Since this was first raised, the reachability of an xcall has been gated at the node level, so this is defense-in-depth, not an open door:
Residual risk
Once an xcall is admitted by the policy gate, it still runs under a real member's full authority in the target context. The target's in-handler env::xcall_origin() check is the last line of defense against a permissive-policy entry point being driven to do more than intended.
Proposed hardening
- Execute admitted xcalls under a least-privilege synthetic/scoped identity rather than
members.first(), so the effective write authority is bounded to what the entry point requires.
- Document the origin-check requirement (
env::xcall_origin()) as a first-class part of the #[app::xcall] contract, so app authors don't rely solely on the node gate.
References
Summary
Cross-context calls (
#[app::xcall]) execute the target method undermembers.first()of the target context — a real owned/admin member with that context's full write authority. We should harden this to run xcalls under a least-privilege scoped identity, so an admitted xcall cannot exercise more authority in the target than the call actually needs.Current state
crates/context/src/handlers/execute/mod.rs:1217:The target then executes via
execute_with_origin(...)undertarget_executor's identity, with the source context tagged asenv::xcall_origin().What already mitigates this (why this is a follow-up, not a live hole)
Since this was first raised, the reachability of an xcall has been gated at the node level, so this is defense-in-depth, not an open door:
#[app::xcall]entry point and the caller must satisfy that entry point's policy (xcall_caller_denied, e.g.from_same_app); unresolvable callers fail closed.xcall_same_owning_group) denies cross-group calls.Residual risk
Once an xcall is admitted by the policy gate, it still runs under a real member's full authority in the target context. The target's in-handler
env::xcall_origin()check is the last line of defense against a permissive-policy entry point being driven to do more than intended.Proposed hardening
members.first(), so the effective write authority is bounded to what the entry point requires.env::xcall_origin()) as a first-class part of the#[app::xcall]contract, so app authors don't rely solely on the node gate.References
crates/context/src/handlers/execute/mod.rs:1217(target selection)crates/context/src/handlers/execute/mod.rs:834-864(node-enforced caller-policy gate)crates/context/src/handlers/execute/mod.rs:1190-1207(owning-group boundary)