GitHub Advisoryadvisory · Go

GHSA-2hw4-c9m4-f2fj knative.dev/pkg admission webhooks allow unbounded request-body memory exhaustion

MEDIUM Published 2026-10-28 Modified 2026-10-28 Official page ↗
Ecosystem
Go
Packages
knative.dev/pkg
Severity
Moderate
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
Weaknesses
CWE-770
Reviewed
2026-10-28

Affected packages

• Go knative.dev/pkg: < 0.0.0-20260531000007-52dbd5ece63f → fixed in 0.0.0-20260531000007-52dbd5ece63f

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` — `main` HEAD on `github.com/knative/pkg` (verified 2026-05-18 against `webhook/admission.go` sha `1ad5a1ce0ee7043081cb50fd1acc6d36a9126844`). • Every release that ships the current `webhook/admission.go` handler. Vulnerable code `webhook/admission.go:109-117`: ```go 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 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. • 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: ```python import socket total = 2 ', 8443)) s.sendall(b'POST / 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): 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): ```go 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])

Related

Weakness · CWE (1)

  • CWE-770 Allocation of Resources Without Limits or Throttling weakness

References