Date: 2026-06-20. Target: GitLab EE @ master HEAD 9e82941 (2026-06-19). White-box, EE source sparse-clone (blobless, app/graphql+policies+ee/lib+ee/app/services+ee/app/models+lib/gitlab/auth, 103MB). No live instance needed (source-only walk; avoided the OOM-risk GitLab-CE docker on the 3.8GB box).
CVE-2026-6552 (CVSS 8.7, patched 2026-06-10 in 19.0.2/18.11.5/18.10.8): improper authorization in the Group SAML Identity API. A Group Owner could take over another member's account via the SAML extern_uid management workflow - no victim credentials, no IdP control needed beyond the owner's normal group-admin rights.
ee/lib/api/provider_identity.rb PATCH /groups/:id/saml/:uid:
identity.assign_attributes(extern_uid: params[:extern_uid])
identity.trusted_extern_uid = false if provider_type == 'saml' && identity.extern_uid_changed?
trusted_extern_uid=true means a SAML login auto-trusts that extern_uid->account binding WITHOUT ownership proof. Pre-fix, a group owner who changed a victim's extern_uid to the owner's own IdP NameID kept trusted_extern_uid=true -> owner logs in via group SAML and the finder returns the VICTIM's user = account takeover. The fix RESETS trust on any change, so the rebind requires the real owner to re-link (proving IdP auth) before login works again.
The protection enforcement: ee/lib/gitlab/auth/group_saml/user.rb:51 (and lib/gitlab/auth/o_auth/user.rb:19) raise IdentityWithUntrustedExternUidError if identity&.persisted? && !identity.trusted_extern_uid?.
Re-trust only via ee/lib/gitlab/auth/group_saml/identity_linker.rb:re_trust_identity - runs ONLY for the signed-in user's own identity AFTER completing SAML link (ownership proven) + email-verified gate. Safe.
The reset is INLINE in the PATCH handler's SAML branch, not a model-level before_save callback. Per the session META heuristic, per-handler fixes are the ones that leave sibling write-sites uncovered. So I enumerated EVERY writer of a group_saml Identity's extern_uid (the model that carries trusted_extern_uid):
provider_identity.rb PATCH API - FIXED (resets trust). Gated by before { authorize_admin_group }.identity_linker.rb:83 (identity.extern_uid = uid.to_s) - login self-service on current_user.identities, gated by current_user.verified_email?(auth_hash.email), re-trusts only after ownership proof. NOT group-owner-against-victim.group_saml/user.rb:106 - enterprise-user login provisioning, requires a valid SAML assertion (IdP control) + email match. Different (IdP-admin) threat model, not the group-owner-API CVE.ee/app/services/ee/users/build_service.rb:66 - builds a group_saml identity at NEW-user SCIM provisioning (extern_uid synced once, default-trusted, brand-new user). No existing victim to hijack.The same PATCH handler also serves provider_type == 'scim' and does NOT reset trust there. This is CORRECT, not an incomplete-fix:
- SCIM writes the GroupScimIdentity/ScimIdentity model, a SEPARATE table that has no trusted_extern_uid and is NOT a login binding (SCIM is provisioning-only; you can't "log in via SCIM").
- The SCIM update path (ee/lib/api/scim/group_scim.rb:106 identity.update(extern_uid:)) writes ONLY scim_identities; it never touches the linked group_saml Identity's extern_uid.
- SCIM->SAML linkage exists ONLY at initial new-user provisioning (build_service dual-create), never on an update path that could rebind an existing victim.
=> A group owner cannot use SCIM to move a victim's SAML login binding. No sibling vuln.
WALK CLEAN. The patched handler is the ONLY group-owner-reachable writer of a SAML login binding; the fix neuters the dangerous auto-trust side-effect there while still permitting legitimate identity management. All other extern_uid writers are either login-flow self-service (ownership-proven) or require IdP control. SCIM exclusion is correct.
A per-handler fix is incomplete ONLY IF sibling handlers share the same dangerous primitive. Before judging completeness, ENUMERATE every write-site of the exact field the fix protects (here: trusted_extern_uid reset gated on extern_uid change to the trust-bearing model). If the patched handler is the ONLY attacker-reachable writer of that field/binding, per-handler == complete coverage -> walk. This sharpens the prior "per-handler fixes leave gaps" heuristic: per-handler is a NECESSARY-not-sufficient signal; the write-site census is the decider.
Corollary: when a multiplexed handler (%w[saml scim].each) applies a fix to only ONE branch, check whether the OTHER branch operates on a DIFFERENT model/trust-domain. If it does, the asymmetry is intentional, not a bug. (Contrast: same-model siblings with asymmetric fixes = the Gitea-feed / Filebrowser-cleanup finds.)
Source-only walk; did NOT stand up GitLab-CE docker (the 3.8GB box would thrash). Correct call: the CVE class is a source-authz audit, dynamic confirmation only needed had a candidate survived the census. None did.