Lesson (cycle295, 2026-06-28): on a multi-CVE-swept authz surface, the residual asymmetries are DESIGN, not gaps
Target: Mattermost plugins (playbooks run handlers + calls host-controls), Bugcrowd mattermost-mbb-public.
What worked
- Read the permission FUNCTION semantics, not just "does the handler call a check." Every playbooks run
handler calls
RunView or RunManageProperties - a naive "find the handler missing a check" grep finds
NOTHING. The real question is what RunView (= owner|participant|PlaybookView, public-playbook-team-wide)
vs RunManageProperties (= owner|participant|admin) actually permit, and whether the WHICH-check
distinction is intentional. The bug, if any, is in the gate's semantics + the storage scope, not its
presence.
- Disclosure-gate FIRST reframed the whole hunt. The first endpoint I'd have flagged (getPlaybookRunMetadata
leaking private-channel name/participants to a playbook-member) is ALREADY CVE-2026-20796, FIXED at HEAD
(the
hasChannelAccess field-filter I read IS the patch). Searching the exact class before any deep dive
saved a wasted live stand-up.
What didn't (the brief's hypothesis didn't hold)
- The brief expected "one handler MISSING the membership/RBAC check its sibling enforces." On a swept surface
that handler doesn't exist - all siblings check. The asymmetries that remain are:
requestUpdate=RunView vs requestJoinChannel=RunManageProperties -> deliberate (viewer-nudge is a
feature; the post leaks nothing back to the attacker).
- self-join (
AddRunParticipants RunView branch) -> participant -> RunManageProperties self-promotion ->
write run state. By-design "Playbooks for Everyone"; the SENSITIVE boundary (channel membership /
messages) is separately + correctly gated by ChannelManageMembers on force-add (the CVE-2026-20796 fix).
- calls
handleDismissNotification missing the ReadChannel check its siblings have -> inert: the write is
hardcoded to DismissedNotification[requesterOwnID], zero victim impact.
The reusable rule
- Saturated-surface tell = count the shipped CVEs in the class. Mattermost playbooks authz =
CVE-2022-1548 + CVE-2026-20796 + CVE-2026-6343 + 5 sweep commits (MM-62690/63410/67867/68844/...). 3+ CVE
strikes on a class = the maintainer's security team is actively sweeping it = surviving-sibling odds are
low. Deprioritize per the saturated-surface doctrine.
- By-design test (doctrine): a by-design sink is only eligible if it defeats a guard the project actually
built+applied elsewhere. Here the project built BOTH the RunView/RunManageProperties write-gate AND the
intentional RunView self-join path; together they MEAN "viewers may gain manage by joining," and the truly
sensitive thing (channel) is gated apart. So the self-promotion is consistent design, not a defeated guard
-> ineligible. Contrast: an eligible incomplete-fix would be a WRITE handler that the sweep left on RunView
AND whose effect crosses the channel boundary the project promised to defend (none found here).
- Inert-write filter: a missing-check write keyed to the requester's OWN id (follow, dismiss-notification)
is an authz asymmetry with no victim impact = not an eligible write/tamper. Bank it only as a watch-item
for future versions that might make the field affect others.
Cross-ref: [[feedback_authz_grep_is_lead_not_finding]] (enumerate gate-helper semantics, batch-fix lowers
sibling odds), [[feedback_shortlist_crossref_findings_first]] (boards/playbooks already walked cycle287/291),
[[feedback_complete_degraded_gate_before_regreenlight]] (saturation moves fast on popular targets).