Live › vulnerability › GHSA-2hw4-c9m4-f2fj
Go to dashboardThe knative.dev/pkg admission webhook handler decodes the incoming request body without a size limit. A single very large POST to a webhook port can make the controller allocate memory until it is OOM-killed.
Summary written from the sources below. Check the originals before acting.
First seen by InfraStack 2026-10-28 21:22 UTC. All times on this page are UTC.
Any Go service built on knative.dev/pkg/webhook is exposed, because the vulnerable handler is shared scaffolding. This covers many Knative, Tekton, KEDA, and Sigstore policy-controller deployments, and any in-house admission webhook that uses this package. Teams whose webhooks do not use this package, or who run a fixed version, can skip it.
| Product | Ecosystem | Affected | Fixed in |
|---|---|---|---|
| knative.dev/pkg | go | < 0.0.0-20260531000007-52dbd5ece63f | 0.0.0-20260531000007-52dbd5ece63f |
go list -m knative.dev/pkg in each Go module that builds a webhook, and check whether the version is earlier than 0.0.0-20260531000007-52dbd5ece63f.go.sum and vendored copies for knative.dev/pkg at an affected version.knative.dev/pkg/webhook.knative.dev/pkg to 0.0.0-20260531000007-52dbd5ece63f or later.Each row is the first time that source carried this signal. The analyst’s rows are its own findings.
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.
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.
knative.dev/pkg — main HEAD on github.com/knative/pkg (verified 2026-05-18 against webhook/admission.go sha 1ad5a1ce0ee7043081cb50fd1acc6d36a9126844).webhook/admission.go handler.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.
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 |
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.
failurePolicy. For projects with failurePolicy=Fail (e.g., Sigstore policy-controller's image-policy enforcement), the consequence is cluster-wide admission outage. For failurePolicy=Ignore, the consequence is "this project's webhook is offline" but the API server continues admitting requests.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.
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.
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.
tonghuaroot ([email protected])
This page as .md · .json · in your agent: get_signal("GHSA-2hw4-c9m4-f2fj")
GitHub Advisory Database, CC-BY 4.0. Posts quoted belong to their authors.
buildpacks-community/kpack |
| Image / Builder validation |
| ... |