Actualités

CVE-2026-19478 : suppression de projets GitLab sans authentification — patch d'urgence, détection et test de restauration de votre forge

Le 17 août 2026, GitLab a publié hors cadence un correctif pour CVE-2026-19478 (CVSS 9.4), une injection de code via une directive GraphQL exploitable sans authentification ni interaction, permettant de modifier ou supprimer des projets publics et des données utilisateurs. Cet article détaille le périmètre exact, une procédure d'inventaire et de patch, les mesures de bord si la fenêtre de maintenance manque, les motifs de détection dans les logs et l'exercice de restauration qui devrait accompagner tout incident de ce type.

| | | 10 min read · by
Console d'administration d'une instance GitLab auto-hébergée affichant une alerte de mise à jour critique

Une faille qui permet de supprimer des projets sans compte, sans clic et sans jeton mérite une fenêtre de maintenance en urgence, y compris au mois d’août. C’est exactement le profil de CVE-2026-19478, corrigée par GitLab le 17 août 2026 dans une release publiée hors de sa cadence habituelle.

Ce qui s’est passé le 17 août

La chronologie est en elle-même un signal. Le 12 août, GitLab publie une release de maintenance de routine, sans vulnérabilité de criticité élevée. Cinq jours plus tard, le 17 août, arrive un patch ad hoc en dehors du rythme habituel (deux mercredis par mois) : ce type d’écart n’est utilisé que lorsque l’éditeur estime que l’attente de la prochaine fenêtre planifiée est un risque en soi.

Deux vulnérabilités sont corrigées.

CVE-2026-19478 (CVSS 9.4) est une injection de code via une directive GraphQL. Les caractéristiques importantes pour l’évaluation du risque : exploitable à distance, sans authentification et sans interaction utilisateur. L’impact documenté par l’éditeur est la modification ou la suppression de projets publics et de données utilisateurs. Autrement dit, il ne s’agit pas d’une fuite d’information sur laquelle on peut temporiser : le scénario le plus défavorable est destructif.

CVE-2026-19650 (CVSS 7.1) est un CSRF dans le gestionnaire de requêtes multiplex GraphQL, qui permet de faire exécuter des mutations via de simples requêtes GET. Prise isolément, elle nécessite qu’un utilisateur authentifié visite une page piégée ; combinée à la première, elle élargit la surface d’attaque et, surtout, elle contredit une hypothèse sur laquelle beaucoup d’équipes ont bâti leurs règles de filtrage : « les mutations passent en POST ».

Périmètre des instances self-managed, CE comme EE :

Branche Versions affectées Version corrigée
18.x de 18.2 jusqu’avant 18.11.11 18.11.11
19.0.x avant 19.0.8 19.0.8
19.1.x avant 19.1.6 19.1.6
19.2.x avant 19.2.4 19.2.4

GitLab.com et GitLab Dedicated sont déjà corrigés côté éditeur : si toute votre chaîne CI/CD est en SaaS, vous n’avez rien à faire. Dans les contextes français où la forge auto-hébergée reste dominante — banques, industrie, secteur public — la question est plutôt de savoir combien d’instances vous avez, et non si vous en avez une.

À ce stade, aucun exploit public ni exploitation constatée n’est rapporté, et GitLab n’a pas encore précisé quelle directive est en cause. C’est précisément ce qui rend la fenêtre courte : les détails techniques sont publiés sur le tracker après le délai de divulgation, et la reconstitution d’un PoC à partir du diff est un exercice de quelques heures pour qui s’y consacre.

Inventaire en 15 minutes

Le principal risque opérationnel n’est pas de rater le patch : c’est de rater une instance. Commencez par un recensement large, incluant les déploiements Omnibus sur VM, le chart Helm, l’opérateur Kubernetes, et les instances « historiques » d’une équipe, d’un laboratoire ou d’une filiale rachetée.

Relever la version précise :

# Omnibus
sudo gitlab-rake gitlab:env:info

# Kubernetes / Helm
kubectl -n gitlab get deploy -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.template.spec.containers[0].image}{"\n"}{end}'

# Depuis l'extérieur, sur une instance dont vous n'avez pas le shell
curl -s https://gitlab.example.com/help | grep -Eo 'GitLab [0-9]+\.[0-9]+\.[0-9]+'

Qualifier ensuite l’exposition, avec trois questions binaires :

  1. /api/graphql est-il joignable depuis Internet, directement ou via le reverse proxy ?
  2. L’instance héberge-t-elle des projets en visibilité publique ?
  3. Y a-t-il un WAF ou un proxy applicatif capable d’inspecter le corps des requêtes en amont ?
curl -s -o /dev/null -w '%{http_code}\n' \
  -X POST https://gitlab.example.com/api/graphql \
  -H 'Content-Type: application/json' \
  --data '{"query":"{ __typename }"}'

Une réponse 200 sans en-tête d’authentification confirme que l’endpoint répond aux requêtes anonymes — comportement normal de GitLab, mais qui devient la porte d’entrée dans ce contexte. Instance exposée + projets publics = patch immédiat, avant la fin de la journée. Instance strictement interne, sans projet public, derrière un VPN : priorité haute mais planifiable, sans oublier que le modèle de menace inclut l’attaquant déjà présent sur le réseau interne.

Patcher proprement

GitLab impose un chemin de montée de version : on ne saute pas arbitrairement de mineures, et certaines versions sont des arrêts obligatoires pour laisser les migrations en arrière-plan se terminer. Vérifiez votre trajectoire avec l’outil officiel de chemin de mise à jour et, dans la console d’administration, que les batched background migrations de la version courante sont bien à l’état terminé avant de démarrer.

Le réflexe qui sauve, avant toute manipulation :

# Sauvegarde applicative (dépôts, base, uploads, artefacts…)
sudo gitlab-backup create STRATEGY=copy

# Les secrets et la configuration ne sont PAS dans l'archive ci-dessus
sudo cp /etc/gitlab/gitlab-secrets.json /secure-store/gitlab-secrets.$(date +%F).json
sudo cp /etc/gitlab/gitlab.rb        /secure-store/gitlab.rb.$(date +%F)

Sans gitlab-secrets.json, une restauration vous rend une base dont les variables CI chiffrées, les tokens et les runners enregistrés sont irrécupérables. C’est l’erreur classique des restaurations « réussies mais inutilisables ».

Points d’attention selon l’architecture : sur une topologie Geo, on met à jour le site primaire puis les secondaires en respectant l’ordre documenté ; les runners doivent rester dans une version compatible avec le serveur ; le registry et les composants externalisés (PostgreSQL, Redis, Gitaly) ont leurs propres contraintes de séquencement. Prévenez les équipes : pendant la fenêtre, la CI est gelée et les push échouent. Une annonce de dix lignes évite trente tickets.

Après redémarrage, contrôlez la version effective, l’état des migrations et la santé des composants :

sudo gitlab-rake gitlab:env:info
sudo gitlab-rake db:migrate:status | grep -c '^\s*down' # attendu : 0
sudo gitlab-ctl status
curl -s https://gitlab.example.com/-/readiness?all=1 | jq .

Si vous ne pouvez pas patcher tout de suite

Certaines organisations ont des fenêtres de changement contractuelles. Dans ce cas, on réduit la surface — sans se raconter d’histoires : ce ne sont pas des correctifs.

Mesures utiles, par ordre d’efficacité :

  • Retirer l’exposition publique de l’instance (filtrage IP, VPN, mise derrière un accès authentifié en amont). C’est la seule mesure qui ferme réellement le vecteur non authentifié.
  • Refuser les mutations en GET sur /api/graphql, ce qui adresse directement CVE-2026-19650.
  • Bloquer les requêtes GraphQL anonymes si vos usages le permettent (attention aux intégrations, badges, et au registry).
  • Abaisser les rate limits GraphQL de l’instance (Admin → Settings → Network) pour ralentir une phase de découverte.

Exemple minimal côté NGINX, à adapter à votre reverse proxy :

location /api/graphql {
    # Aucune mutation par GET, en aucun cas
    if ($request_method = GET) { return 405; }

    # Refuser les requêtes anonymes : ni cookie de session, ni en-tête d'autorisation
    set $has_auth 0;
    if ($http_authorization)          { set $has_auth 1; }
    if ($http_cookie ~* "_gitlab_session") { set $has_auth 1; }
    if ($has_auth = 0)                { return 403; }

    limit_req zone=graphql burst=10 nodelay;
    proxy_pass http://gitlab-workhorse;
}

Le passage des projets publics en visibilité interne réduit ce qu’un attaquant peut atteindre par les chemins normaux, mais ne garantit rien face à une injection qui contourne l’autorisation : traitez-le comme une limitation d’impact, pas comme une protection.

Détection et investigation

Premier geste : geler les journaux avant rotation. Sur Omnibus, /var/log/gitlab/gitlab-rails/graphql_json.log, api_json.log et production_json.log, plus les logs d’accès du reverse proxy et, en EE, les audit events. Copiez-les hors de la machine avant toute intervention.

Les motifs à rechercher :

# Requêtes GraphQL portant des directives (@...) — les noms de champs varient selon la version
jq -r 'select(.query_string? // "" | test("@")) |
       [.time, .remote_ip? // "-", .operation_name? // "-", (.query_string|.[0:200])] | @tsv' \
  graphql_json.log

# Mutations passées en GET côté proxy
grep -E 'GET /api/graphql' access.log | grep -iE 'mutation|query=%7Bmutation'

# Pics de 200/500 sur l'endpoint, par minute
awk '$0 ~ "/api/graphql" {print substr($4,2,17), $9}' access.log | sort | uniq -c | sort -rn | head

Côté intégrité, vérifiez ce qui compte vraiment : projets manquants, changements de visibilité, modifications de groupes, comptes altérés, clés SSH ou tokens ajoutés. La corbeille des projets (suppression différée) est votre meilleure amie ici : elle conserve les projets supprimés pendant une période de rétention configurable, mais limitée. Consultez-la immédiatement, avant que le délai n’expire.

sudo gitlab-rails runner 'Project.where.not(pending_delete: false).or(Project.where.not(marked_for_deletion_at: nil)).each { |p| puts [p.full_path, p.marked_for_deletion_at].join(" ") }'

Enfin, cadrez la fenêtre d’antériorité. « Pas de PoC public » ne signifie pas « pas d’exploitation » : la vulnérabilité existe depuis la 18.2. En pratique, on investigue en priorité la période récente pour laquelle les logs sont complets, et l’on documente honnêtement la limite au-delà de laquelle la rétention ne permet plus de conclure.

Le vrai test : savoir restaurer sa forge

Une vulnérabilité destructive teste moins votre patching que votre capacité de restauration. Trois vérités à rappeler :

  1. Une archive gitlab-backup seule ne restaure pas une instance fonctionnelle. Il faut l’archive plus gitlab-secrets.json plus la configuration, et une version de GitLab identique à celle de la sauvegarde pour lancer gitlab-backup restore.
  2. Une réplique Geo n’est pas une sauvegarde. Une suppression malveillante se réplique fidèlement vers les sites secondaires. Geo répond à la disponibilité et à la latence, pas à la destruction de données.
  3. Un objet storage versionné, un snapshot de volume et une sauvegarde applicative ne couvrent pas les mêmes scénarios. Sachez lequel répond à « un projet a été supprimé mardi à 14 h ».

L’exercice, à faire cette semaine sur un environnement bac à sable :

# Restauration complète sur une instance jetable de même version
sudo gitlab-ctl stop puma && sudo gitlab-ctl stop sidekiq
sudo cp /secure-store/gitlab-secrets.json /etc/gitlab/
sudo gitlab-backup restore BACKUP=1755...  # timestamp de l'archive
sudo gitlab-ctl reconfigure && sudo gitlab-ctl restart
sudo gitlab-rake gitlab:check SANITIZE=true

Mesurez deux chiffres et écrivez-les dans le runbook : le RTO réel (temps écoulé jusqu’à un premier git push réussi) et le RPO réel (perte de données acceptée). Vous constaterez souvent que la restauration d’un dépôt de plusieurs dizaines de gigaoctets, avec ses artefacts et son registry, se compte en heures et non en minutes. Une forge est une dépendance critique de la chaîne de livraison : elle mérite le même niveau d’exigence qu’un système de production.

Le fond : pourquoi la couche GraphQL récidive

Plusieurs analyses relèvent qu’il s’agit de la troisième vulnérabilité majeure de la couche GraphQL de GitLab en 2026. Directives, handler multiplex, mutations acceptées en GET : ce n’est pas un accident, c’est un motif. GraphQL déplace une partie du contrôle d’accès du routeur HTTP vers le résolveur, et introduit des primitives d’exécution (directives, alias, fragments, batching) qui s’exécutent avant ou en marge de la logique métier. Chaque couche ajoutée entre le parsing et l’autorisation est une occasion de contourner cette dernière.

Ce qui se transpose directement à vos propres API GraphQL :

  • Autoriser avant de résoudre, et ne jamais dépendre du champ demandé pour décider de l’accès.
  • Interdire les mutations en GET au niveau du transport, pas seulement de la convention.
  • Désactiver les directives non utilisées et refuser les directives inconnues à l’analyse du document.
  • Allow-list de requêtes persistées pour les clients internes : c’est la mesure la plus efficace, et la plus contraignante.
  • Limites de profondeur, de complexité et de taille de batch sur le handler multiplex.
  • Journaliser par opération nommée, avec identité de l’appelant : sans cela, votre investigation post-incident se résume à des lignes POST /api/graphql 200.

Checklist et suivi

[ ] Inventaire de TOUTES les instances self-managed (VM, Helm, opérateur, filiales)
[ ] Version relevée et comparée au périmètre (18.11.11 / 19.0.8 / 19.1.6 / 19.2.4)
[ ] Exposition qualifiée : /api/graphql public ? projets publics ?
[ ] Sauvegarde archive + gitlab-secrets.json + gitlab.rb, copiée hors machine
[ ] Chemin de montée de version validé, migrations en arrière-plan terminées
[ ] Patch appliqué, version et migrations vérifiées, readiness OK
[ ] Runners / Geo / registry remis en cohérence
[ ] Logs gelés avant rotation, recherche de directives et de mutations en GET
[ ] Contrôle d'intégrité : projets, groupes, comptes, corbeille projets
[ ] Restauration testée sur bac à sable, RTO/RPO réels documentés

Deux points à relire dans les semaines qui viennent : la publication des détails techniques sur le tracker GitLab à l’expiration du délai de divulgation — c’est à ce moment que la probabilité d’exploitation opportuniste augmente nettement — et une éventuelle inscription au catalogue KEV, qui déclencherait des obligations de délai pour certaines organisations. D’ici là, le seul indicateur qui compte est le nombre d’instances de votre inventaire encore en version vulnérable : s’il n’est pas à zéro ce soir, vous savez ce qu’il reste à faire demain matin.

Comments (0)

Please log in to leave a comment.

Log In

No comments yet. Be the first to comment!