Skip to main content

merge_permissions_capability

Function merge_permissions_capability 

Source
pub fn merge_permissions_capability(
    trusted: Option<&CapabilityConfig>,
    project: Option<&CapabilityConfig>,
) -> Option<CapabilityConfig>
Expand description

Merge a project-layer capabilities.permissions table onto the trusted (user/global) layer’s — the single canonical merge BOTH the CLI route (crates/cli/src/userconfig.rs::overlay_project) and this core resolver (resolve_top, below) call, so the two routes cannot diverge the way the independent Fable-5 review of P4a found (proven attacks, both against the hard approval floor Config::needs_approval gives rules.deny — true even under ApprovalPolicy::Never):

  • Attack A (whole-table replace): a per-capability insert (what the CLI’s overlay_project used to do, and what a naive per-name merge would still do here) lets a hostile project’s [capabilities. permissions] table — even one sanitize_for_project/ sanitized_for_project strips down to an EMPTY table because every key it set was forbidden — wholesale REPLACE the trusted layer’s populated table, silently wiping rules.deny and everything else the user set. Fixed by deep-merging into a CLONE of the trusted table (via merge_json_object) rather than ever substituting the project’s.
  • Attack B (array-replace widens deny): merge_json_object’s “arrays replace wholesale” rule (§3.3 “tables merge key-wise… arrays replace”) is correct for rules.allow (a widening allow is already stripped from a sanitized project layer by P1/P4a) but WRONG for rules.deny: a project’s own deny = […] would otherwise REPLACE, not add to, the trusted layer’s list — e.g. user deny = ["bash*"] + project deny = ["harmless*"] merging to ["harmless*"] is a real widening (the floor that blocks bash* vanishes). Fixed by unioning rules.deny explicitly after the deep merge: a project may only ADD deny entries, never remove or shrink the trusted layer’s — deny strictly grows.
  • Attack B’, P5-1 extension: the identical array-replace hazard applies to TWO more keys the P5-1 permissions engine newly consumes: rules.ask (module 11) and protected_paths.paths (module 13). Both are narrowing-only by the SAME argument as deny — an ask entry can only make a decision STRICTER (it is checked before allow, and can never override a deny), and a protected path is an unconditional deny floor for read+write — so a project may only ADD to either, never silently wipe the trusted layer’s via protected_paths.paths = []/ rules.ask = []. Fixed the same way: union both, right alongside rules.deny, immediately below.

rules.allow and every other key keep plain deep-merge/replace semantics: this function does not re-derive the sanitizer’s trust decisions (that’s sanitize_for_project/sanitized_for_project’s job), it only guarantees the MERGE step can’t reintroduce a widening those sanitizers already ruled out.

No behavior change for the common case: with no project permissions table, this returns the trusted layer’s table unchanged.