
Analyse des causes profondes, lab Docker vulnérable et scripts PoC pour CVE-2026-85706, une lecture arbitraire de fichiers non authentifiée dans GitLab via un différentiel d'analyseur.
CVSS 10.0 · Non authentifiée · Exploitée activement (CISA KEV)
Une divergence d'analyse syntaxique entre GitLab Workhorse (proxy inverse en Go) et Puma/Grape
(Ruby) permet à un attaquant non authentifié de contourner la délégation d'upload accéléré de Workhorse et
d'atteindre trois endpoints d'upload avec un file.path contrôlé par l'attaquant, permettant une lecture
arbitraire de fichiers sur l'hôte GitLab.
Encoder un caractère de files en %66iles fait que la route d'upload de Workhorse échoue (elle
correspond sur le chemin encodé) tandis que Rails le décode et atteint le vrai handler (il
route sur le chemin décodé). Le handler fait confiance au paramètre brut file.path que Workhorse
était censé écraser — ainsi file.path=/etc/passwd est lu depuis le disque sans aucun identifiant.
POST /api/v4/projects/1/repository/%66iles/x?file=&file.path=/etc/passwd&file.size=1
^^^^^^ Workhorse misses -> raw file.path survives to Rails
| Version | |
|---|---|
| Affecté | CE/EE 18.7 → 19.1.8, 19.2 → 19.2.6, 19.3 → 19.3.2 |
| Corrigé | 19.1.8 / 19.2.6 / 19.3.2 (2026-09-10) |
| Chemin | Contenu |
|---|---|
docs/ANALYSIS.md | Analyse complète de la cause racine — divergence d'analyse syntaxique, les deux JWT Workhorse, le bug de confiance au file.path brut, le diff du patch, et le canal d'exfiltration par erreur réfléchie. Analyse réalisée sur le code source réel (v19.3.1-ee vs v19.3.2-ee). |
lab/ | Lab vulnérable basé sur Docker (gitlab-ce:19.3.1-ce.0) + instructions de mise en route. |
poc/ | detect.sh (oracle d'existence non destructif) et exploit.sh (lecture de fichier par erreur réfléchie). Cible unique, soumis à autorisation. |
Vous n'êtes pas familier avec les rouages internes de GitLab ? Voici ce que fait chaque couche — dans
l'ordre où une requête les traverse. Un glossaire plus approfondi avec les concepts clés se trouve dans
docs/ANALYSIS.md §0.
Internet
│
▼
┌────────┐ ┌───────────┐ ┌────────┐ ┌──────────────────────┐
│ NGINX │────▶│ Workhorse │────▶│ Puma │────▶│ Grape / Rails app │
│(proxy) │ │ (Go) │ │ (Ruby) │ │ (Ruby) │
└────────┘ └───────────┘ └────────┘ └──────────────────────┘
| Composant | Description |
|---|---|
| NGINX | Proxy inverse le plus externe. Termine le TLS, sert les fichiers statiques, transmet tout le reste vers l'intérieur. Non directement impliqué dans cette vulnérabilité. |
| Workhorse | Un proxy inverse en Go spécifique à GitLab. Son rôle principal est de décharger le travail pour lequel Ruby est lent — en particulier le streaming de gros uploads. Pour les endpoints d'upload, Workhorse met le corps en mémoire tampon dans un fichier temporaire, signe un JWT, et réécrit file.path afin que Ruby ne voie jamais les octets bruts de l'upload. Il appose également un JWT Gitlab-Workhorse-Api-Request par requête sur chaque requête qu'il transmet (prouvant « ceci est passé par le proxy », pas « cet utilisateur est authentifié »). |
| Puma | Le serveur d'application Ruby qui exécute l'application Rails. Reçoit les requêtes de Workhorse, exécute les middlewares (Rack), et les distribue au routeur. |
| Rack | La couche d'interface du serveur web Ruby. Le middleware Rack gère l'analyse de la query-string, la gestion de session, et — point critique ici — la vérification du JWT d'upload de Workhorse et la construction des objets UploadedFile. Rack::Utils.parse_nested_query est la fonction dont les messages d'erreur divulguent le contenu des fichiers dans cet exploit. |
| Grape | Un framework d'API REST utilisé par GitLab pour tous les endpoints /api/v4/*. Fournit les définitions de routes et les before-filters comme require_gitlab_workhorse! (vérification du proxy) et authenticate! (vérification de l'identité de l'utilisateur). S'exécute dans Rails, sur Puma, derrière Workhorse — il voit donc le chemin d'URL décodé. |
| Rails | Le framework web global (Ruby on Rails). GitLab est un monolithe Rails — modèles, services et middlewares s'exécutent tous ici, dans Puma. |
La vulnérabilité réside dans l'écart entre Workhorse (qui fait correspondre les routes sur le chemin encodé) et Grape/Puma (qui routent sur le chemin décodé). Voir ci-dessous.
r.URL.EscapedPath() (encodé) ;
Puma/Grape route sur le chemin décodé. %66iles ≠ regex files pour Workhorse, mais
se décode en files pour Rails.file.path vers un chemin temporaire signé, et ne définit jamais
l'en-tête Gitlab-Workhorse-Multipart-Fields — mais il transmet quand même la requête (avec un
JWT Gitlab-Workhorse-Api-Request valide).authenticate!, et le handler
lisait params['file.path'] directement (la chaîne de l'attaquant) au lieu de l'UploadedFile
vérifié par Workhorse et confiné au chemin params[:file].Rack::Utils.parse_nested_query ("invalid %-encoding (<file bytes>)") — la réponse reflète les octets du fichier
jusqu'au premier % invalide.Le patch ajoute authenticate!, bascule vers l'UploadedFile vérifié, et cesse de
refléter e.message. Voir docs/ANALYSIS.md §5.
# 1. Stand up the vulnerable lab (see lab/README.md for details)
cd lab && docker compose up -d # wait ~5 min for GitLab to become healthy
# 2. Non-destructive detection
../poc/detect.sh http://localhost:8929
# 3. File-read PoC against a file you're authorized to read on your own lab
../poc/exploit.sh http://localhost:8929 /var/opt/gitlab/gitlab-rails/etc/gitlab.yml
Ceci est publié à des fins défensives et éducatives : comprendre, détecter et corriger une CVE divulguée et corrigée qui figure sur la liste CISA KEV. Les scripts ici sont à cible unique et exigent que vous passiez la cible explicitement.
localhost. Ne le pointez pas vers des hôtes tiers.Si vous exécutez GitLab, mettez à niveau vers une version corrigée — c'est la seule véritable remédiation.
gitlab-org/gitlab @ v19.3.1-ee vs v19.3.2-eeMIT — analyse et code PoC uniquement. GitLab est une marque de GitLab Inc. ; ce dépôt n'est ni affilié à GitLab Inc. ni approuvé par celle-ci.