{"id":"CVE-2026-54247","aliases":["GHSA-cwxq-rc9x-2jvv","GO-2026-6015"],"url":"https://o3.security/vulnerability/CVE-2026-54247","summary":"Skipper: Unbounded Request Body Read in Admission Webhook Causes Memory Exhaustion DoS","details":"## Summary\n\nThe Kubernetes admission webhook handler reads the entire request body using `io.ReadAll(r.Body)` without any size limit. Any client that can reach the webhook port within the cluster can send a multi-GB payload, causing the skipper process to exhaust memory and be OOM-killed. This disrupts all Kubernetes admission control, potentially blocking all pod creation and updates.\n\n## Vulnerable Code\n\n```go\n// dataclients/kubernetes/admission/admission.go:76\nbody, err := io.ReadAll(r.Body)  // <-- NO SIZE LIMIT\nif err != nil {\n    log.Errorf(\"Failed to read request: %v\", err)\n    w.WriteHeader(http.StatusInternalServerError)\n    invalidRequests.WithLabelValues(admitterName).Inc()\n    return\n}\n\nvar review admissionReview\nerr = json.Unmarshal(body, &review)\n```\n\nFor comparison, the OPA filter has a body size limit:\n\n```go\n// filters/openpolicyagent/openpolicyagent.go:68-70\nconst DefaultMaxRequestBodySize = 1 << 20 // 1MB\n\n// OPA uses a bufferedBodyReader with size limits\n```\n\n## Attack Path\n\n1. Attacker identifies the admission webhook endpoint (default: `:9443/admission` or configured path)\n2. Attacker sends: `POST /admission HTTP/1.1, Content-Type: application/json` with a multi-GB request body\n3. `io.ReadAll(r.Body)` allocates unbounded memory for the entire body\n4. Skipper process is OOM-killed by the Kubernetes kubelet\n\n## Permission Boundary Analysis\n\n- **Attacker**: Any client with network access to the admission webhook port within the Kubernetes cluster\n- **Boundary crossed**: Memory safety — unbounded allocation from attacker-controlled input\n- **Preconditions**: Admission webhook endpoint must be network-reachable (default Kubernetes deployment exposes it within cluster network)\n- **Comparison**: OPA filter has `DefaultMaxRequestBodySize` (1MB) and semaphore-based memory limit; admission handler has neither\n\n## Evidence\n\n| File | Lines | Description |\n|------|-------|-------------|\n| `dataclients/kubernetes/admission/admission.go` | 76 | `io.ReadAll(r.Body)` without size limit |\n| `filters/openpolicyagent/openpolicyagent.go` | 68-70 | OPA filter has `DefaultMaxRequestBodySize` = 1MB |\n| `filters/openpolicyagent/openpolicyagent.go` | 1333-1336 | OPA uses `bufferedBodyReader` with size limits |\n\n## Tests\n\n- `dataclients/kubernetes/admission/admission_test.go` exists but **does not test body size limits**\n\n## Impact\n\nThe admission webhook handler reads the entire request body using io.ReadAll(r.Body) without a size limit. An attacker with in-cluster network access and a valid Kubernetes client certificate can send a multi-GB payload to the webhook endpoint, causing the skipper process to exhaust memory and be OOM-killed. This disrupts admission control for Ingress and RouteGroup resources until the process is automatically restarted by the kubelet.\n\nScope of impact: Ingress and RouteGroup admission only — not pod creation or other admission controllers.\n\nRecovery: Kubernetes automatically restarts the OOM-killed process, limiting downtime.\n\nPrerequisites: (1) In-cluster network access to the webhook port, (2) valid Kubernetes client certificate.\n\n## Mitigation\n\n1. Add `http.MaxBytesReader` or equivalent body size limit before `io.ReadAll`\n2. Follow the OPA filter pattern: define `DefaultMaxRequestBodySize` and use a buffered reader with size limits\n3. Add a configurable `--admission-max-body-size` flag","published":"2026-09-14T20:04:06.550Z","modified":"2026-10-28T02:31:04.215016490Z","cvss":{"score":4.3,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L"},"epss":{"score":0.00227,"percentile":0.13603,"asOf":"2026-09-16"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Go","name":"github.com/zalando/skipper","fixedVersion":"0.26.22"}],"fix":{"url":"https://github.com/zalando/skipper/commit/c3d156c6caaefecc9a46774c0f9c3c70b97160b4","label":"zalando/skipper@c3d156c"},"references":[{"type":"WEB","url":"https://github.com/zalando/skipper/releases/tag/v0.26.22"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/54xxx/CVE-2026-54247.json"},{"type":"ADVISORY","url":"https://github.com/zalando/skipper/security/advisories/GHSA-cwxq-rc9x-2jvv"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-54247"},{"type":"FIX","url":"https://github.com/zalando/skipper/commit/c3d156c6caaefecc9a46774c0f9c3c70b97160b4"},{"type":"FIX","url":"https://github.com/zalando/skipper/pull/4053"},{"type":"PACKAGE","url":"https://github.com/zalando/skipper"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-10-28T02:31:04.215016490Z"}}