Skip to content

audit F8 [security]: RevokeAdmin against genesis author succeeds with multi-admin majority #608

@intendednull

Description

@intendednull

File: crates/state/src/materialize.rs:222
Severity: security
Obvious? no

apply_proposed_action::RevokeAdmin only refuses to apply when state.admins.len() == 1 && state.admins.contains(peer_id) (line 224). There is no exemption for state.genesis_author.

Bypass: once two admins exist, a malicious admin emits Propose { action: RevokeAdmin { peer_id: genesis_author } }; if a second admin votes yes (or threshold = Count(1) was set earlier), check_and_apply_proposal fires state.admins.remove(&genesis_author). After that point the genesis owner can no longer satisfy is_admin() in check_permission (materialize.rs:107), so every subsequent Propose/Vote they sign is rejected — including the owner_override path on line 197-200 (gated on prop.proposer being admin via the upstream check). Impact: collusion of any admin majority permanently strips the server owner of the "unilateral push" they are documented to retain in docs/specs/2026-04-12-state-authority-and-mutations.md and CLAUDE.md ("Owner is root of trust").

Fix: in apply_proposed_action::RevokeAdmin, add if state.genesis_author == Some(*peer_id) { return; } ahead of state.admins.remove(peer_id) (and the same guard for KickMember, since kick also strips admin and removes membership). The owner role must be structurally non-revocable, exactly the way Permission::__UnknownLegacy is structurally inert.


Filed by /general-audit @ 88498a5 (2026-05-04). master: #600.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions