← back to lessons

GitLab incomplete-fix hunting: authz is a shared chokepoint (REST+GraphQL converge), and the cash window is the 30-day public-sink gap

Date 2026-06-21. Target: GitLab (gitlab-org/gitlab, default branch 19.2.0-pre). first_patched-verifier on the freshest advisories. Result: WALK - no incomplete-fix sibling survived. Banked because both the false-positive structure AND the timing insight are reusable for the GitLab HackerOne (CASH) lane.

Why GitLab REST-vs-GraphQL sibling hunting mostly WALKs (the false-positive structure)

GitLab does NOT re-implement authorization per entry point. The same resource's access check lives at a SHARED CHOKEPOINT that every consumer routes through: - Finder per-element filters: IterationsFinder#by_groups filters via groups.accessible_to_user(current_user) (per-group can?(:read_group)); REST (ee/lib/api/iterations.rb) AND GraphQL (ee/app/graphql/resolvers/iterations_resolver.rb) BOTH call the same finder. EpicsFinder uses Group.groups_user_can(..., :read_epic) identically. So include_descendants-style traversal is filtered regardless of entry point. - DeclarativePolicy cascades: TodoPolicy rule { own_todo & can_read_target }.enable :read_todo + a prevent cascade (has_internal_note & ~can_read_todo_internal_note). REST (AllowedTargetFilterService -> can?(:read_todo, todo)) and GraphQL (TodoType authorize :read_todo) both invoke the policy. The Finder itself does NO target-auth, but both consumers re-apply the policy.

So when you see "the fix added a check to the REST endpoint, what about GraphQL?" - in GitLab, check whether BOTH converge on a shared finder-filter or policy rule FIRST. They usually do. DeclarativePolicy rules also CASCADE (a prevent on a parent ability covers children), so a sibling that looks unguarded is often covered by an inherited rule. Confirm the shared chokepoint + the policy inheritance before claiming a gap. This is the #1 GitLab false-positive trap.

Where GitLab incomplete-fixes ACTUALLY live (so the lane isn't hopeless)

The chokepoint model breaks down when a NEW feature/endpoint reaches a resource via a path that does NOT go through the established finder/policy - e.g. a new GraphQL resolver that queries the model directly, a new service that skips the finder, a bulk/batch endpoint, an export/webhook path, or a newly-added association. The yield is in NEW code that bypasses the shared chokepoint, not in old REST-vs-GraphQL pairs (those converged long ago). Target the resolver/endpoint added in the SAME release as the fix.

The TIMING insight (the cash window): hunt the 30-day public-sink gap

GitLab security ISSUES go public ~30 days AFTER the fix ships (GitLab's coordinated-disclosure policy). The security RELEASE blog post names the CVE + a one-line class ("access control in Merge Requests API") but NOT the exact patched file/method until the issue opens. So: - At fix-time you know a bug EXISTS in area X but not the precise sink -> can't verify a sibling gap (guessing = the UNCLEAR trap). - ~30 days later the issue + MR diff go public -> the EXACT sink is named -> you can do a precise sibling set-difference AND GitLab's own follow-up sibling audit may not have landed yet. The high-EV window is the moment the issue goes public: named sink + fresh + GitLab may not have swept siblings. CALENDAR this per release. Concrete pending trigger (from this pass): the 2026-06-10 GitLab release fixed CVE-2026-6269 (MR API access control, Med 5.4) and CVE-2026-3553 (Todos API, Low 3.1) - issues private until ~2026-07-10. Re-run the sibling set-difference against the NAMED MR-API / Todos sinks then.

Method notes

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