CVE-2026-45662

CVSS 8.8 · Élevée Corrigé
Type
Recherche de vulnérabilités
Publié le
Durée de lecture
6 min de lecture
Auteur
rootzero
Read in English
Read in English →

CVE-2026-45662 : injection de commandes dans Dokploy via l’URL de registre lors du docker logout

Dokploy échappait l’URL de registre pour docker login mais pas pour docker logout : supprimer un registre piégé exécutait des commandes sur l’hôte.

Pour décider

Impact
La suppression d’un registre peut exécuter des commandes sur l’hôte Dokploy qui pilote Docker.
Conditions
Un compte doit pouvoir configurer et supprimer un registre. L’analyse précise la voie de stockage et ses limites.
Action recommandée
Installer Dokploy 0.29.3 ou ultérieur et vérifier les droits de gestion des registres.

Fiche de l’avis

Produit
Dokploy
Paquet
npm · @dokploy/server
Versions affectées
< 0.29.3
Corrigé dans
0.29.3
CVSS
8.8 Élevée
Faiblesse
CWE-78
Accès requis
Compte requis
Avis publié
Crédit
rootzero

Synthèse de l’enquête

À la suppression d’un registre, Dokploy auto-hébergé lançait docker logout en concaténant l’URL enregistrée dans une commande shell, alors que le même fichier échappait cette valeur pour docker login. Un utilisateur autorisé à créer et supprimer des registres pouvait enregistrer une URL contenant des métacaractères shell et exécuter des commandes sur l’hôte Dokploy, qui pilote Docker et détient les secrets de la plateforme. Corrigé en 0.29.3.

Login échappé, logout oublié

Nous avons pris au sérieux un ancien correctif de Dokploy : registry.ts contenait déjà shEscape() et une construction sûre pour docker login. Nous avons donc cherché les commandes voisines qui manipulaient la même URL de registre. Plus bas dans le fichier, removeRegistry() l’insérait directement dans docker logout, exécuté par un helper fondé sur child_process.exec(). Le schéma d’entrée ne demandait qu’une chaîne.

Nous avons construit la preuve autour d’une URL forgée conservée dans un registre, puis lue lors de sa suppression. Le détail qui comptait était transactionnel : une création peut être annulée si docker login échoue, alors que registry.update conserve la valeur avant sa tentative de connexion. docker logout, au moment de la suppression, atteignait ensuite le shell. Nous avons signalé le cas le 29 mars 2026. Dokploy a ajouté l’échappement au point d’exécution et une validation de l’URL le 9 mai, publié 0.29.3 et CVE-2026-45662 le 11, puis protégé les dernières commandes de connexion en 0.29.8. La précondition est un compte capable de gérer les registres, pas une requête anonyme.

Une commande shell de chaque côté du registre

Dokploy gère des registres Docker sur les serveurs qu’il pilote. Ajouter un registre appelle docker login ; le supprimer appelle docker logout. Les deux commandes sont construites par un processus Node qui accède à l’environnement Docker de l’hôte.

L’essentiel de ce que fait Dokploy se termine par une commande shell. Son utilitaire execAsync() est un child_process.exec() transformé en promesse, ce qui veut dire que chaque chaîne de commande passe par /bin/sh -c. Les registres de conteneurs font partie des fonctions construites ainsi : quand un utilisateur ajoute un registre, Dokploy lance docker login, et quand il le supprime, Dokploy lance docker logout.

Le correctif du login et le chemin du logout

packages/server/src/services/registry.ts montre déjà une conscience du problème. Un changement de septembre 2025 (« fix: use shellsafe docker command ») y a introduit une fonction shEscape() et un utilitaire safeDockerLoginCommand() qui protège le registre, l’identifiant et le mot de passe avant de construire la commande docker login.

Nous avons vérifié chaque commande shell construite à partir d’un registre. Plus bas dans le même fichier, removeRegistry() construit sa commande docker logout par simple interpolation. La valeur interpolée, registryUrl, est validée par z.string() dans le schéma Zod, sans aucune restriction de caractères.

Cause racine

La construction sûre et la construction vulnérable se trouvent dans le même fichier :

// packages/server/src/services/registry.ts (0.29.2)
function shEscape(s: string | undefined): string {
	if (!s) return "''";
	return `'${s.replace(/'/g, `'\\''`)}'`;
}

export function safeDockerLoginCommand(registry, user, pass) {
	const escapedRegistry = shEscape(registry);
	const escapedUser = shEscape(user);
	const escapedPassword = shEscape(pass);
	return `printf %s ${escapedPassword} | docker login ${escapedRegistry} -u ${escapedUser} --password-stdin`;
}

export const removeRegistry = async (registryId: string) => {
	// ... suppression de la ligne, conservée dans `response`
	if (!IS_CLOUD) {
		await execAsync(`docker logout ${response.registryUrl}`);
	}
	return response;
};

shEscape() place une valeur entre apostrophes et réécrit toute apostrophe interne en '\'', ce qui est la bonne façon de transmettre une chaîne arbitraire comme un seul mot à un shell POSIX. removeRegistry() s’en passe. L’URL enregistrée devient une partie d’un programme shell, si bien qu’un point-virgule termine docker logout et démarre une nouvelle commande (CWE-78). La condition IS_CLOUD limite ce chemin aux instances auto-hébergées, qui sont précisément celles qui exécutent des commandes sur leur propre hôte.

Exploitation

La chaîne repose sur deux opérations tRPC : enregistrer une URL de registre contenant des métacaractères du shell, puis supprimer ce registre. Le chemin fiable pour conserver cette valeur est registry.update sur un registre existant : registry.create peut annuler son insertion si le docker login initial échoue. Le compte doit disposer des permissions de modification et de suppression de registre.

À la suppression, le serveur exécute :

docker logout ; id > /tmp/pwned #

Le # final met en commentaire tout ce qui pourrait suivre. Dans le code de la 0.29.2, createRegistry() insère la ligne et lance docker login dans une même transaction de base de données. updateRegistry() écrit les nouvelles valeurs avant la tentative de connexion, hors de cette transaction. La suppression n’a besoin que de la valeur stockée ; le docker logout vulnérable ne dépend d’aucun registre joignable. Notre rapport initial proposait une création directe sans tenir compte du possible retour arrière transactionnel : cette séquence n’est donc pas une preuve fiable à elle seule.

Impact

L’avis GitHub note la faille 8,8 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H). L’attaquant a besoin d’un compte dans l’organisation dont le rôle accorde les permissions sur les registres : dans le routeur de la 0.29.2, registry.create et registry.update exigent withPermission("registry", "create") et registry.remove exige withPermission("registry", "delete"). Gérer des registres est une tâche d’exploitation que les plateformes confient couramment aux développeurs, ce qui correspond à PR:L.

L’attaquant obtient l’exécution de commandes avec les privilèges du processus serveur de Dokploy, sur l’hôte principal. Ce processus lance les commandes Docker de toute la plateforme : la commande injectée peut donc démarrer des conteneurs, lire leur environnement et monter des chemins de l’hôte. Il a aussi accès à la base de données de la plateforme, avec les secrets de chaque projet, et aux clés SSH qui servent à joindre les serveurs distants. Une permission prévue pour gérer des identifiants d’images devient le contrôle de la plateforme de déploiement et, à travers elle, de chaque application qu’elle héberge.

Correctif

Le correctif est le commit 547ba2d, daté du 09/05/2026 et livré dans Dokploy 0.29.3 le 11/05/2026. L’avis lui-même indique la plage de versions <= 0.0.0 sans version corrigée, ce qui est incorrect : la première étiquette de version contenant le commit est v0.29.3, et la ligne docker logout de v0.29.2 n’est toujours pas échappée.

Le commit traite les deux extrémités du flux. Le point d’exécution applique l’échappement existant :

// packages/server/src/services/registry.ts (0.29.3)
await execAsync(`docker logout ${shEscape(response.registryUrl)}`);

Le point d’entrée reçoit un format strict pour les URL de registre, utilisé par les schémas de création, de test et de mise à jour (apiUpdateRegistry dérive du même createSchema) :

// packages/server/src/db/schema/registry.ts (0.29.3)
const registryUrlSchema = z
	.string()
	.refine(
		(val) =>
			val === "" ||
			/^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?(:\d{1,5})?$/.test(val),
		"Registry URL must be a valid hostname or hostname:port (e.g. registry.example.com or localhost:5000)",
	);

Les deux niveaux se complètent. La validation bloque les nouvelles valeurs malveillantes, et l’échappement au point d’exécution neutralise aussi une valeur enregistrée avant la mise à jour, que la validation seule laisserait passer.

Le même motif a survécu ailleurs quelques semaines de plus. Le commit 1f4f940 (#4579), publié dans la 0.29.8 le 08/06/2026, a remplacé deux autres commandes de connexion construites à la main, dans utils/cluster/upload.ts et utils/providers/docker.ts, qui interpolaient le mot de passe du registre entre guillemets doubles (où $(...) reste interprété) et l’identifiant entre apostrophes. Les deux passent désormais par safeDockerLoginCommand().

Détection et mitigation

  1. Mettre à jour Dokploy vers la 0.29.3 ou une version ultérieure, de préférence la 0.29.8 ou au-delà, qui fait aussi passer les dernières constructions docker login par l’utilitaire d’échappement.
  2. Examiner les registres enregistrés dans la base Dokploy et rechercher toute valeur de registryUrl qui n’est pas un simple nom d’hôte ou hôte:port ; une valeur contenant ;, |, &, $, des backticks, des guillemets, des espaces ou # signale une tentative.
  3. Vérifier quels rôles détiennent les permissions de création et de suppression sur registry, et les réserver aux personnes qui gèrent les identifiants d’images.
  4. Utiliser le journal d’audit de Dokploy, qui enregistre la création, la modification et la suppression des registres, pour repérer des registres créés puis supprimés en peu de temps ou par des comptes inattendus.
  5. Sur l’hôte, surveiller les processus dont le parent est le sh -c docker logout ... lancé par le processus Node.js de Dokploy, et plus largement les shells, curl, wget ou outils réseau démarrés par ce processus en dehors des déploiements.

Chronologie

Date Événement
29/03/2026 Signalement privé aux mainteneurs via un avis de sécurité GitHub
09/05/2026 Échappement au point d’exécution et validation de l’URL intégrés au code (547ba2d)
11/05/2026 Publication de Dokploy 0.29.3 et de l’avis GHSA-827c-7x62-29jq avec CVE-2026-45662
08/06/2026 Publication de Dokploy 0.29.8, avec les dernières commandes de connexion échappées (1f4f940, #4579)

Références

  1. Avis de l’éditeur · CVE-2026-45662 github.com/Dokploy/dokploy/security/advisories/GHSA-827c-7x62-29jq
  2. Fiche NVD · CVE-2026-45662 nvd.nist.gov/vuln/detail/CVE-2026-45662
  3. Fiche CVE.org · CVE-2026-45662 www.cve.org/CVERecord?id=CVE-2026-45662
  4. MITRE CWE-78 cwe.mitre.org/data/definitions/78.html
  5. Dépôt du projet github.com/Dokploy/dokploy

Citer cet avis

rootzero. « CVE-2026-45662 : injection de commandes dans Dokploy via l’URL de registre lors du docker logout ». Recherche de vulnérabilités, rootzero, 11 mai 2026. https://rootzero.eu/research/cve/cve-2026-45662-dokploy-command-injection/

#Recherche de vulnérabilités

Faites auditer votre code avant qu’un autre ne le fasse.

Votre équipe doit décider quoi corriger avant une livraison ou évaluer une dépendance critique. Nous définissons une mission autour de cette décision, avec des preuves et des priorités de correction expliquées.