← back to lessons

GitLab CVE-2026-6269 (hidden-MR modification, developer role) incomplete-fix = WALK CLEAN

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

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.

Fix (the primitive)

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 incomplete-fix hypothesis (and why it WALKS)

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.

Banked methodology refinement (reusable)

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.

Verdict

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).

Generated 2026-07-02 13:15:03 UTC | auto-sync /15min