Problem
crates/state/src/materialize.rs:290-292 documents that SetProfile, PinMessage, and UnpinMessage are "unrestricted (any member)" events, but no membership check is actually enforced:
// SetProfile — unrestricted (any member)
// PinMessage,
// UnpinMessage — unrestricted (any member)
When a member is kicked, they are removed from state.members (line 221). However, if a late-arriving event from the kicked member is received after the kick is processed, it will be applied without checking membership:
SetProfile (line 472): Updates profiles without checking state.members
PinMessage (line 505): Pins messages without checking membership
UnpinMessage (line 514): Unpins messages without checking membership
Compare with RotateChannelKey (line 494-496) which correctly checks membership:
if !state.members.contains_key(&event.author) {
return ApplyResult::Rejected(format!("author '{}' is not a member", event.author));
}
Severity
MEDIUM — kicked members can continue modifying server state (profiles, pins). Limited impact since they can't send messages or escalate privileges, but violates the principle that kicked members have zero authority.
Fix
Add the same membership check used by RotateChannelKey to the unrestricted event handlers:
EventKind::SetProfile { .. } | EventKind::PinMessage { .. } | EventKind::UnpinMessage { .. } => {
if !state.members.contains_key(&event.author) {
return ApplyResult::Rejected(format!("author '{}' is not a member", event.author));
}
// ... existing logic
}
Update the spec (docs/specs/2026-04-12-state-authority-and-mutations.md) to clarify that "unrestricted" means "any current member" not "anyone."
Locations
crates/state/src/materialize.rs:472-483 — SetProfile (no membership check)
crates/state/src/materialize.rs:505-511 — PinMessage (no membership check)
crates/state/src/materialize.rs:514-520 — UnpinMessage (no membership check)
crates/state/src/materialize.rs:494-496 — RotateChannelKey (correct pattern)
Problem
crates/state/src/materialize.rs:290-292documents thatSetProfile,PinMessage, andUnpinMessageare "unrestricted (any member)" events, but no membership check is actually enforced:When a member is kicked, they are removed from
state.members(line 221). However, if a late-arriving event from the kicked member is received after the kick is processed, it will be applied without checking membership:SetProfile(line 472): Updates profiles without checkingstate.membersPinMessage(line 505): Pins messages without checking membershipUnpinMessage(line 514): Unpins messages without checking membershipCompare with
RotateChannelKey(line 494-496) which correctly checks membership:Severity
MEDIUM — kicked members can continue modifying server state (profiles, pins). Limited impact since they can't send messages or escalate privileges, but violates the principle that kicked members have zero authority.
Fix
Add the same membership check used by
RotateChannelKeyto the unrestricted event handlers:Update the spec (
docs/specs/2026-04-12-state-authority-and-mutations.md) to clarify that "unrestricted" means "any current member" not "anyone."Locations
crates/state/src/materialize.rs:472-483— SetProfile (no membership check)crates/state/src/materialize.rs:505-511— PinMessage (no membership check)crates/state/src/materialize.rs:514-520— UnpinMessage (no membership check)crates/state/src/materialize.rs:494-496— RotateChannelKey (correct pattern)