Date: 2026-06-20. Source: GitLab EE @ master HEAD (sparse: app/policies + ee/app/policies + app/graphql/mutations + ee/app/graphql/mutations + ee/app/services/merge_requests). White-box, no instance.
CVE-2026-6269 (CVSS 5.4, patched 06-11, reported via H1 by rogerace): an authenticated developer could perform unauthorized actions on a HIDDEN merge request (a banned/spam user's MR). CE+EE, 15.10..before 18.10.8/18.11.5/19.0.2.
app/policies/merge_request_policy.rb:
rule { hidden & ~admin }.policy do
prevent :read_merge_request
prevent :update_merge_request
prevent :approve_merge_request
end
hidden = @subject.hidden? (author banned). The fix prevents exactly 3 abilities for non-admins on a hidden MR.
The prevent-list is only 3 of the ~8 MR abilities. Abilities NOT in the list looked like gaps:
- set_merge_request_metadata, create/delete_merge_request_work_item_relation (gated on can?(:admin_merge_request), which is a PROJECT ability NOT prevented by the MR-scoped hidden rule - confirmed admin_merge_request only prevented when merge_requests_disabled | repository_disabled, project_policy.rb:513).
- create_merge_request_approval_rules (EE, gated only on a license condition).
- reopen_merge_request (only prevented by locked).
Each collapses at the CONSUMER (reachability) layer:
1. reopen_merge_request / update_merge_request: enabled via rule { can?(:read_merge_request) & assignee_or_author } (issuable_policy.rb). hidden prevents read -> not enabled. Cascades off read. COVERED.
2. set_merge_request_metadata: its ONLY consumer is ee/app/services/merge_requests/reviewer_assignment/assign_service.rb, whose own comment states AutoAssignReviewersWorker is "the only caller". Worker-internal, NOT a user-facing API/GraphQL mutation. NOT attacker-reachable.
3. The user-facing reviewer/assignee/label/milestone/draft/lock mutations: the GraphQL MR mutation Base class app/graphql/mutations/merge_requests/base.rb:21 declares authorize :update_merge_request, INHERITED by set_reviewers, set_assignees, set_labels, set_milestone, set_draft, set_locked, update, request_changes, reviewer_rereview, etc. -> all gate on update_merge_request, which the fix PREVENTS on hidden. COVERED.
4. Overriding mutations: accept->accept_merge_request (prevented via ~read), create->create_merge_request_from (creation, not modification of an existing hidden MR), set_subscription->update_subscription (enabled only via can?(read) -> not enabled on hidden), update_approval_rule->update_approvers (gated on approval_rules_editable = can?(update_merge_request) -> prevented). ALL COVERED.
Net: every user-facing MR MODIFICATION mutation funnels through update_merge_request (the Base authorize) or an ability that cascades off read; the fix's 3 prevents cover that entire surface. The non-cascaded abilities are worker-internal, not wired to a hidden-MR-reachable handler. WALK CLEAN.
A POLICY-layer ability gap is only a real finding if a REACHABLE HANDLER authorizes on that specific uncovered ability. When auditing a policy fix that prevents abilities {A,B,C}, do NOT stop at "ability D is not prevented" - trace D to its CONSUMER:
- If D's only consumer is a background worker / internal service -> not attacker-reachable -> no finding.
- If the user-facing handler layer centralizes authorize on a COVERED ability (here: GraphQL MR mutation Base -> update_merge_request), the per-ability policy gap is unreachable -> the fix is complete despite looking partial.
This is the consumer-layer companion to the same-day GitLab CVE-2026-6552 write-site-census rule and the Gitea dual-enforcement-model rule: a fix that looks per-handler/partial at one layer can be complete because a DIFFERENT layer centralizes the gate. Always verify the layer where attacker input actually lands.
WALK CLEAN. 4th consistent-fix walk this session (GitLab 6552, Gitea wrr5+fhx7, GitLab 6269) - reconfirms well-maintained projects close authz via a centralizing layer; the edge pays only on a genuinely INCONSISTENT cross-sibling fix at the SAME reachable layer (the cycle239 Gitea feed outlier).