Neutron, notre moteur d’IA, a obtenu un score de 97,1 % sur le benchmark CyberGym de l’UC Berkeley. En savoir plus

Analyse approfondie

Analyse approfondie

Kubeflow Pipelines : exécution de code entre locataires (CVE-2026-77596)

Kubeflow Pipelines avant 2.18.0 omet l'autorisation des pipelines partagés, laissant un locataire exécuter du code ailleurs. Corrigé en 2.18.0.

En bref

  • Un locataire à faibles privilèges peut envoyer un pipeline partagé, ou une nouvelle version d'un pipeline partagé existant, sans SubjectAccessReview Kubernetes. L'avis a été publié le 28 octobre 2026.
  • Kubeflow Pipelines avant 2.18.0 est concerné en mode multi-utilisateur lorsque REQUIRE_NAMESPACE_FOR_PIPELINES vaut false, ce qui est la valeur par défaut. Une exécution récurrente non épinglée qui suit la dernière version exécute alors le pipeline injecté dans l'espace de noms d'un autre locataire.
  • L'enregistrement CVE le note CVSS v3.1 9,6 Critical. Aucune preuve de concept publique ni exploitation dans la nature n'est signalée, et la CVE n'est pas au catalogue KEV de la CISA au 6 novembre 2026.
  • Mettez à niveau vers Kubeflow Pipelines 2.18.0 ou ultérieur. Si vous ne pouvez pas, exigez un espace de noms sur les opérations de pipeline et placez un proxy authentifiant devant le serveur d'API ; voir « Comment corriger ? ».

Suis-je concerné ?

Vous êtes exposé uniquement lorsque toutes ces conditions sont réunies :

  • Le cluster exécute Kubeflow Pipelines avant 2.18.0. L'avis liste les versions jusqu'à 2.17.3 comme concernées.
  • Le serveur d'API fonctionne en mode multi-utilisateur. Les installations mono-utilisateur contournent entièrement le chemin d'autorisation et ne sont pas concernées par cette faille.
  • REQUIRE_NAMESPACE_FOR_PIPELINES vaut false. La valeur par défaut est false et elle n'est pas définie dans les manifestes fournis, donc une installation multi-utilisateur par défaut est exposée.
  • Le serveur d'API est joignable par l'attaquant. S'il est joignable directement, sans proxy authentifiant, un appelant non authentifié peut utiliser le contournement. Derrière un proxy authentifiant, l'appelant a besoin d'une identité de locataire valide à faibles privilèges.
  • Pour l'étape d'exécution de code, l'espace de noms cible possède une exécution récurrente non épinglée qui suit la dernière version du pipeline. Une exécution épinglée, ou une exécution qui embarque sa spécification, n'est pas remplacée.

Le serveur d'API est le déploiement ml-pipeline dans l'espace de noms kubeflow. Ses points d'entrée HTTP d'envoi sont POST /apis/v2beta1/pipelines/upload et POST /apis/v2beta1/pipelines/upload_version, les anciens chemins v1beta1 étant servis par le même serveur.

Branche Versions concernées Version corrigée
Kubeflow Pipelines toutes les versions avant 2.18.0 (avis : jusqu'à 2.17.3) 2.18.0 (publiée le 28 octobre 2026)

Vérifiez le tag de l'image et le réglage d'espace de noms :

kubectl -n kubeflow get deploy ml-pipeline -o jsonpath='{range .spec.template.spec.containers[*]}{.name}{" "}{.image}{"\n"}{end}'
kubectl -n kubeflow get deploy ml-pipeline -o yaml | grep -i REQUIRE_NAMESPACE_FOR_PIPELINES

Un tag d'image inférieur à 2.18.0, sans entrée REQUIRE_NAMESPACE_FOR_PIPELINES ou avec la valeur false, correspond à la configuration par défaut exposée. Quand la variable est absente, le serveur utilise false.

Comment corriger ?

  1. Mettez à niveau vers Kubeflow Pipelines 2.18.0 ou ultérieur. La version 2.18.0 est la première qui contient le correctif d'autorisation. Appliquez les manifestes correspondant à votre méthode d'installation.
  2. Si vous ne pouvez pas mettre à niveau maintenant, définissez REQUIRE_NAMESPACE_FOR_PIPELINES=true sur le serveur d'API. La validation rejette alors un espace de noms vide avant que l'envoi n'atteigne la branche non protégée. C'est une mesure d'atténuation, pas le correctif. Un appelant peut encore nommer un espace de noms qu'il ne possède pas, et la vérification qui suit est celle que le patch répare.
  3. Placez un proxy authentifiant devant le serveur d'API et ne l'exposez pas directement. L'avis indique qu'un serveur joignable directement permet à un appelant non authentifié d'utiliser le contournement. Un proxy qui authentifie chaque requête supprime ce chemin.
  4. Auditez les pipelines partagés et leurs versions pour les images de conteneur, commandes et arguments que vous ne reconnaissez pas, et examinez les exécutions récurrentes qui suivent la dernière version.

L'avis du fournisseur ne nomme aucun contournement au niveau du code. La mise à niveau est la seule correction complète.

Quelle est la cause racine ?

Le chemin d'envoi HTTP renvoie un succès depuis son assistant d'autorisation dès que l'espace de noms cible est vide, donc un envoi de pipeline partagé n'atteint jamais la SubjectAccessReview. L'assistant dans backend/src/apiserver/server/pipeline_upload_server.go à la version 2.17.3 :

func (s *PipelineUploadServer) canUploadVersionedPipeline(r *http.Request, pipelineID string, resourceAttributes *authorizationv1.ResourceAttributes) error {
    if !common.IsMultiUserMode() {
        // Skip authorization if not multi-user mode.
        return nil
    }
    if len(pipelineID) > 0 {
        namespace, err := s.resourceManager.FetchNamespaceFromPipelineId(pipelineID)
        if err != nil {
            return util.Wrap(err, "Failed to authorize with the Pipeline ID")
        }
        if len(resourceAttributes.Namespace) == 0 {
            resourceAttributes.Namespace = namespace
        }
    }
    if resourceAttributes.Namespace == "" {
        return nil
    }

    resourceAttributes.Group = common.RbacPipelinesGroup
    // ... IsAuthorized() runs only after this point
}

Un espace de noms vide est la façon dont Kubeflow Pipelines désigne un pipeline partagé à l'échelle du cluster. C'est aussi la valeur par défaut quand un appelant omet le paramètre de requête namespace. La validation qui pourrait le rejeter ne s'exécute que si IsNamespaceRequiredForPipelines() est vrai :

func ValidateNamespaceRequired(namespace string) error {
    if common.IsMultiUserMode() &&
        common.IsNamespaceRequiredForPipelines() &&
        namespace == "" {
        return util.NewInvalidInputError("Namespace is required for pipeline operations")
    }
    return nil
}

IsNamespaceRequiredForPipelines() lit REQUIRE_NAMESPACE_FOR_PIPELINES avec une valeur par défaut false. Aucun manifeste sous manifests/ ne la définit.

Le chemin gRPC pour la même écriture est canAccessPipeline() dans pipeline_server.go. Il fait l'inverse : il remappe un espace de noms vide vers l'espace de noms système pour create, update et delete. Un commentaire y indique que les écritures de pipelines partagés doivent tout de même être autorisées. L'assistant HTTP est donc une omission et non une exception voulue.

Comment la faille est-elle exploitée ?

  1. L'attaquant atteint le serveur d'API. Sans proxy authentifiant, aucun compte n'est nécessaire. Derrière un proxy, un compte de locataire à faibles privilèges suffit.
  2. L'attaquant envoie un POST multipart à /apis/v2beta1/pipelines/upload sans paramètre namespace, ou à /apis/v2beta1/pipelines/upload_version avec l'identifiant d'un pipeline partagé existant.
  3. canUploadVersionedPipeline() voit un espace de noms vide et renvoie nil. Aucune SubjectAccessReview n'est envoyée.
  4. L'envoi crée un nouveau pipeline partagé, ou attache une nouvelle version à un pipeline partagé existant. La version porte l'image de conteneur, la commande et les arguments de l'attaquant.
  5. Dans l'espace de noms d'un autre locataire, une exécution récurrente non épinglée qui suit la dernière version récupère la spécification injectée.
  6. L'exécution lance le conteneur de l'attaquant sous le compte de service de cette exécution dans l'espace de noms victime. Les permissions Kubernetes de ce compte s'appliquent alors au code de l'attaquant.

La forme de la requête est un envoi multipart KFP standard. Aucun code de preuve de concept public n'est publié ; l'avis décrit les points d'entrée et la vérification manquante.

Comment se déroule l'attaque ?

Un espace de noms vide contourne l'appel d'autorisation, et une version de pipeline partagé transporte le conteneur de l'attaquant dans l'exécution récurrente d'un autre locataire.

flowchart LR A["Appelant à faibles privilèges ou anonyme"] --> B["POST d'envoi de pipeline sans espace de noms"] B --> C["canUploadVersionedPipeline voit un espace de noms vide"] C --> D["Aucune SubjectAccessReview n'est envoyée"] D --> E["Le pipeline partagé ou la nouvelle version est stocké"] E --> F["L'exécution récurrente non épinglée d'un autre locataire suit la dernière version"] F --> G["Le conteneur de l'attaquant s'exécute avec le compte de service de l'exécution victime"]

Que peut faire un attaquant ?

Un attaquant peut exécuter un conteneur arbitraire dans l'espace de noms d'un autre locataire, sous le compte de service de l'exécution récurrente de ce locataire. Toutes les conditions sont listées ici :

  • Kubeflow Pipelines avant 2.18.0.
  • Le mode multi-utilisateur, avec REQUIRE_NAMESPACE_FOR_PIPELINES à false ou absent.
  • Le serveur d'API joignable par l'attaquant, directement ou via un proxy authentifiant qui accepte un compte à faibles privilèges.
  • Un pipeline partagé existant pour le chemin d'injection de version, ou la permission de créer un nouveau pipeline partagé pour le chemin de création.
  • Dans l'espace de noms victime, une exécution récurrente non épinglée qui suit la dernière version.

L'impact comprend la lecture des secrets de l'espace de noms que le compte de service de l'exécution peut lire, la modification de ses ressources et l'usage de sa position réseau.

L'impact exact dépend des role bindings du compte de service. Une exécution épinglée, ou une exécution qui embarque sa propre spécification, n'est pas remplacée par ce chemin. L'avis ne démontre pas de chemin non authentifié quand un proxy authentifiant protège le serveur.

Pourquoi le correctif fonctionne-t-il ?

Le correctif associe un espace de noms vide à l'espace de noms système de KFP. Il laisse la SubjectAccessReview existante décider, donc une écriture de pipeline partagé est autorisée comme toute autre écriture. Le patch dans pipeline_upload_server.go :

    if len(pipelineID) > 0 {
        namespace, err := s.resourceManager.FetchNamespaceFromPipelineId(pipelineID)
        if err != nil {
            return util.Wrap(err, "Failed to authorize with the Pipeline ID")
        }
        if len(resourceAttributes.Namespace) == 0 {
            resourceAttributes.Namespace = namespace
        }
    }
-   if resourceAttributes.Namespace == "" {
-       return nil
+   if s.resourceManager.IsEmptyNamespace(resourceAttributes.Namespace) {
+       // Shared-pipeline writes require authorization in the KFP system namespace.
+       resourceAttributes.Namespace = common.GetPodNamespace()
    }

    resourceAttributes.Group = common.RbacPipelinesGroup

Le code appelle désormais IsAuthorized() pour les écritures partagées, avec l'espace de noms dans lequel le serveur d'API s'exécute.

Un appelant qui ne peut pas créer de pipelines dans cet espace de noms est refusé. Le correctif ajoute aussi une matrice de tests recordingSubjectAccessReviewClient. Elle couvre les envois partagés et avec espace de noms, les envois de version, les cas autorisés et refusés, et le mode mono-utilisateur. La branche d'espace de noms vide est donc exercée en CI.

Le correctif est complet pour les deux points d'entrée HTTP nommés par l'avis. Il ne change pas la conception RBAC : un locataire qui possède déjà la permission d'écriture des pipelines partagés la conserve, et la décision d'autorisation appartient au cluster.

Comment vérifier si j'ai été compromis ?

Le fournisseur ne publie aucun indicateur de compromission. Vérifiez le journal du serveur d'API et l'inventaire des pipelines partagés :

# Pipeline upload requests and authorization failures in the API server log.
kubectl -n kubeflow logs deploy/ml-pipeline --since=720h | grep -Ei 'upload|SubjectAccessReview|Authorization Failure'

# Shared pipelines visible through the API.
kubectl -n kubeflow port-forward deploy/ml-pipeline 8888:8888 &
curl -s http://localhost:8888/apis/v2beta1/pipelines | head -c 2000

Dans une installation multi-utilisateur, un pipeline sans espace de noms est un pipeline partagé. Examinez chaque pipeline partagé et ses versions, et comparez l'image de conteneur, la commande et les arguments de chaque version avec ce que vos équipes attendent. Listez ensuite les exécutions récurrentes qui suivent la dernière version et vérifiez leur historique récent pour des images que vous n'avez pas déployées.

Les journaux d'audit Kubernetes enregistrent les appels SubjectAccessReview lorsque la politique d'audit les inclut. Au niveau d'audit Request ou supérieur, le corps de la revue inclut l'identité de l'appelant. Recherchez la requête d'envoi dans les journaux d'accès de ml-pipeline ou du proxy, si la journalisation des requêtes est activée. L'absence de SubjectAccessReview n'est pas journalisée en elle-même.

Un envoi réussi est silencieux dans l'interface KFP au-delà du nouveau pipeline ou de la nouvelle version. Considérez un pipeline partagé inattendu, ou une nouvelle version d'un pipeline partagé que vous n'avez pas publiée, comme une piste à examiner.

Chronologie

  • 2026-09-09 : le correctif est fusionné via la pull request #14352.
  • 2026-10-28 : Kubeflow Pipelines 2.18.0 est publié et l'avis GHSA-rjr3-43j3-4q8g est publié.
  • 2026-11-05 : la CVE-2026-77596 est publiée avec CVSS v3.1 9,6.
  • 2026-11-06 : aucune exploitation dans la nature n'est signalée, et la CVE n'est pas au catalogue KEV de la CISA.

Le correctif a été fusionné 49 jours avant la version 2.18.0, et l'enregistrement CVE a suivi 8 jours après la version.
Figure 1 : Du commit de correction à la version 2.18.0 et à l'enregistrement CVE. Source : commit 62516d8 de Kubeflow Pipelines, version 2.18.0 et enregistrement CVE, vérifiés le 6 novembre 2026

FAQ

Une installation mono-utilisateur de Kubeflow Pipelines est-elle concernée ?

Non. L'assistant revient immédiatement quand le mode multi-utilisateur est désactivé, donc le chemin d'autorisation et la branche d'espace de noms vide ne sont jamais utilisés. La faille concerne les installations multi-utilisateur.

L'attaquant a-t-il besoin d'un compte Kubernetes ?

Non lorsque le serveur d'API est joignable directement sans proxy authentifiant ; l'avis indique qu'un appelant non authentifié peut utiliser le contournement dans ce cas. Derrière un proxy authentifiant, l'attaquant a besoin d'un compte de locataire valide à faibles privilèges.

Le code de l'attaquant s'exécute-t-il toujours dans l'espace de noms d'un autre locataire ?

Non. Il s'exécute lorsque l'espace de noms cible possède une exécution récurrente non épinglée qui suit la dernière version. Une exécution qui épingle une version, ou qui embarque sa spécification, n'est pas remplacée par ce chemin.

Suffit-il de définir REQUIRE_NAMESPACE_FOR_PIPELINES=true ?

Cela bloque le chemin d'espace de noms vide, qui est le contournement décrit par cette CVE. C'est une mesure d'atténuation, pas le correctif, et elle ne remplace pas la vérification d'autorisation ajoutée par le patch. Mettez à niveau dès que possible.

La faille a-t-elle été exploitée dans la nature ?

Aucune exploitation dans la nature n'est signalée au 6 novembre 2026, et la CVE n'est pas au catalogue KEV de la CISA. L'avis a été publié le 28 octobre 2026, donc l'absence de signalements est récente.

Comment savoir quelle version j'exécute ?

Vérifiez le tag de l'image du déploiement ml-pipeline. Les manifestes épinglent un tag d'image par version, et 2.18.0 est la première version corrigée. Si votre fournisseur de plateforme intègre Kubeflow Pipelines, demandez quelle version amont le lot contient.

Références

  • Avis de sécurité Kubeflow Pipelines GHSA-rjr3-43j3-4q8g, publié le 28 octobre 2026, avec les versions concernées, le code vulnérable, le correctif et l'impact : https://github.com/kubeflow/pipelines/security/advisories/GHSA-rjr3-43j3-4q8g (vérifié le 6 novembre 2026)
  • Enregistrement CVE-2026-77596, publié le 5 novembre 2026, CVSS v3.1 9.6 et plage concernée : https://www.cve.org/CVERecord?id=CVE-2026-77596 (vérifié le 6 novembre 2026)
  • Pull request Kubeflow Pipelines #14352, le correctif : https://github.com/kubeflow/pipelines/pull/14352 (vérifié le 6 novembre 2026)
  • Commit Kubeflow Pipelines 62516d8c525f146b754cb515cf441dd0290ad841, « fix(backend): authorize shared pipeline uploads (#14352) », 9 septembre 2026 : https://github.com/kubeflow/pipelines/commit/62516d8c525f146b754cb515cf441dd0290ad841 (vérifié le 6 novembre 2026)
  • Version Kubeflow Pipelines 2.18.0, 28 octobre 2026 : https://github.com/kubeflow/pipelines/releases/tag/2.18.0 (vérifié le 6 novembre 2026)
  • Fichier backend/src/apiserver/server/pipeline_upload_server.go de Kubeflow Pipelines à la version 2.17.3, l'assistant vulnérable : https://github.com/kubeflow/pipelines/blob/2.17.3/backend/src/apiserver/server/pipeline_upload_server.go (vérifié le 6 novembre 2026)
  • Fichier backend/src/apiserver/server/pipeline_server.go de Kubeflow Pipelines à la version 2.17.3, le canAccessPipeline() gRPC qui autorise les écritures partagées : https://github.com/kubeflow/pipelines/blob/2.17.3/backend/src/apiserver/server/pipeline_server.go (vérifié le 6 novembre 2026)
  • Fichier backend/src/apiserver/validation/namespace.go de Kubeflow Pipelines à la version 2.17.3, ValidateNamespaceRequired() : https://github.com/kubeflow/pipelines/blob/2.17.3/backend/src/apiserver/validation/namespace.go (vérifié le 6 novembre 2026)
  • Fichier manifests/kustomize/base/pipeline/ml-pipeline-apiserver-deployment.yaml de Kubeflow Pipelines à la version 2.18.0, le déploiement ml-pipeline : https://github.com/kubeflow/pipelines/blob/2.18.0/manifests/kustomize/base/pipeline/ml-pipeline-apiserver-deployment.yaml (vérifié le 6 novembre 2026)
  • Flux JSON du catalogue des vulnérabilités exploitées de la CISA, vérifié pour CVE-2026-77596 le 6 novembre 2026 (non listée) : https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json (vérifié le 6 novembre 2026)
  • GitHub, kubeflow/pipelines, 4 262 étoiles et 2 185 forks : https://github.com/kubeflow/pipelines (vérifié le 6 novembre 2026)