
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 Workhorse/Puma.
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 prise en charge des téléversements accélérés de Workhorse et
d'atteindre trois points de terminaison de téléversement avec un file.path contrôlé par l'attaquant, ce qui donne une lecture
arbitraire de fichiers sur l'hôte GitLab.
Encoder un caractère de files en %66iles fait que la route de téléversement de Workhorse échoue (elle
correspond sur le chemin encodé) tandis que Rails le décode et atteint le vrai gestionnaire (il
route sur le chemin décodé). Le gestionnaire fait confiance au paramètre brut file.path que Workhorse
était censé écraser — donc 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 échoue -> le file.path brut survit jusqu'à 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 de Workhorse, le bug de confiance au file.path brut, le diff du correctif, 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, conditionné à l'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 | Ce que c'est |
|---|---|
| NGINX | Proxy inverse le plus externe. Termine le TLS, sert les fichiers statiques, transmet tout le reste vers l'intérieur. Pas 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 la diffusion en flux de gros téléversements. Pour les points de terminaison de téléversement, 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 du téléversement. Il appose aussi 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 chaîne de requête, la gestion de session, et — point critique ici — la vérification du JWT de téléversement 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 points de terminaison /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 gestionnaire
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 (<octets du fichier>)") — la réponse reflète les octets du fichier
jusqu'au premier % invalide.Le correctif 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 exploitez 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-ee