Résumé
Skipper has OPA body-authz bypass: truncated_body mitigation fails open on chunked/HTTP-2 (incomplete fix GHSA-8qqm-fp2q-v734)
Détails de l’avis
- Affected component:
filters/openpolicyagent/openpolicyagent.go→ExtractHttpBodyOptionally; combined withgithub.com/open-policy-agent/opa-envoy-pluginenvoyauth/request.go→getParsedBody/checkIfHTTPBodyTruncated. Filter:opaAuthorizeRequestWithBody. - Affected versions:
<= 0.27.33(current HEADe7d7014c). Thetruncated_bodymitigation was introduced/recommended in v0.27.26 (advisory GHSA-8qqm-fp2q-v734) and remains bypassable. - Fix chain being audited: CVE-2026-50197 (GHSA-659f-rgp5-w4wf, commit
3152f3b0) → GHSA-8qqm-fp2q-v734 (docs + code, commit1be950cd#4126, v0.27.26). This finding is the third, still-open variant.
Summary
Skipper's opaAuthorizeRequestWithBody filter authorizes requests by handing the (bounded) request body to Open Policy Agent. When a body exceeds -open-policy-agent-max-request-body-size (default 1 MB), Skipper truncates it before OPA sees it. Advisory GHSA-8qqm-fp2q-v734 established that deny-on-presence / body-inspecting policies fail OPEN on oversized bodies, and its remediation instructs policy authors to guard on input.attributes.request.http.truncated_body (implemented as the top-level input.truncated_body):
default allow := false
allow if {
input.truncated_body == false
# ... body-based conditions
}
That mitigation is itself incomplete. The truncated_body flag is computed by the OPA envoy plugin only when a content-length header is present. A request sent with Transfer-Encoding: chunked (HTTP/1.1) or over HTTP/2 carries no content-length, so truncated_body is left false even though Skipper truncated the body. The mitigated policy therefore evaluates input.truncated_body == false as true and ALLOWS the request, while the full, un-inspected oversized payload is forwarded to the upstream (Skipper's bufferedBodyReader streams the buffered prefix and then continues draining the original body).
The transport that defeats the mitigation — chunked / HTTP-2 without Content-Length — is the exact transport class the original CVE-2026-50197 was about; the GHSA-8qqm fix closed the declared-Content-Length variant and its positive-control test only exercised small chunked bodies, never oversized chunked bodies.
Root cause
opa-envoy-plugin/envoyauth/request.go:
func getParsedBody(...) (any, bool, error) {
if val, ok := headers["content-type"]; ok {
if strings.Contains(val, "application/json") {
...
if val, ok := headers["content-length"]; ok { // <-- only path that sets truncation
truncated, err := checkIfHTTPBodyTruncated(val, int64(len(body)))
...
if truncated { return nil, true, nil }
}
...
} else if ... "application/x-www-form-urlencoded" { /* same content-length gate */ }
else if ... "multipart/form-data" { /* same content-length gate */ }
}
return data, false, nil // <-- no content-length ==> truncated_body = false
}
func checkIfHTTPBodyTruncated(contentLength string, bodyLength int64) (bool, error) {
cl, _ := strconv.ParseInt(contentLength, 10, 64)
if cl != -1 && cl > bodyLength { return true, nil }
return false, nil
}
Truncation can only ever be signalled by comparing content-length against the received body length. With chunked/HTTP-2 there is no content-length, so the comparison is skipped and truncated_body is reported false. Skipper (ExtractHttpBodyOptionally) meanwhile does truncate the chunked body to maxBodyBytes (it sets expectedSize = maxBodyBytes when req.ContentLength < 0), producing the exact divergence: OPA is told "not truncated", but the body was truncated, and the backend receives the whole thing.
Reachability
- Deployment runs
opaAuthorizeRequestWithBodywith a body-inspecting policy that follows the GHSA-8qqm mitigation (allow if input.truncated_body == false). This is the maintainer-recommended configuration (advisory + v0.27.26 docs). - Attacker sends a request whose body exceeds
max-request-body-sizeusingTransfer-Encoding: chunked(or HTTP/2). Nocontent-lengthheader is present. ExtractHttpBodyOptionallyreads/truncates the body tomaxBodyBytes;rawBodyis the truncated prefix.AdaptToExtAuthRequestforwards the lowercased headers (nocontent-length) and the truncatedRawBodyto OPA.getParsedBodyfinds nocontent-length→truncated_body = false; for a non-parsed content-type it also returnsparsed_body = nullwith no error.- Policy:
input.truncated_body == falseis satisfied →allow = true. - Skipper forwards the request;
bufferedBodyReader.Readserves the buffered prefix and then continues reading the originalreq.Body, delivering the full oversized payload to the upstream.
Every guard on the path is accounted for: the only "guard" is truncated_body, and it is defeated by omitting content-length.
Impact
Bypass of OPA request-body authorization for any deployment that adopted the official truncated_body mitigation. Requests that the policy is meant to reject (oversized / un-inspectable bodies, or bodies whose forbidden content lies beyond the inspection window) are authorized and forwarded in full to the protected upstream. Same security property and severity class as CVE-2026-50197 / GHSA-8qqm (both High).
CVSS 3.1
Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N = 7.5 High
- AV:N — remote HTTP request.
- AC:L — single crafted request; no race/special conditions (just chunked framing + padding).
- PR:N / UI:N — unauthenticated, no user interaction.
- S:U — impact within the authorized component/upstream trust scope.
- C:N — no direct disclosure by the bypass itself.
- I:H — authorization control is bypassed; forbidden request content reaches the upstream (integrity of the access-control decision / protected resource).
- A:N — not primarily an availability issue.
(Conditional: exploitable only where opaAuthorizeRequestWithBody is used with a truncated_body-gated policy — i.e. deployments that followed the GHSA-8qqm mitigation. Consistent with the conditional nature of the parent CVEs.)
Proof of Concept
Executable Go test added to the OPA filter package. It stands up a real Skipper proxy (proxy.WithParams) + a real OPA control plane (opasdktest) with WithMaxRequestBodyBytes(32) and the advisory's verbatim mitigation policy (allow if input.truncated_body == false), routed to a recording upstream.
- Positive control — oversized body with Content-Length →
truncated_body=true→ 403 (mitigation works). - Bypass — identical oversized body sent chunked (no Content-Length) →
truncated_body=false→ 200, and the upstream receives the full 66-byte payload past the 32-byte inspection cap.
Command and observed benign output:
$ go test ./filters/openpolicyagent/ -run TestTruncatedBodyChunkedBypass -count=1 -v
poc_truncated_body_chunked_test.go:148: [content-length ] status=403 upstream_body_bytes=-1
poc_truncated_body_chunked_test.go:154: [chunked ] status=200 upstream_body_bytes=66
--- PASS: TestTruncatedBodyChunkedBypass (0.11s)
PASS
- content-length variant → 403, upstream received nothing (
-1) — mitigation works. - chunked variant (byte-identical body) → 200, upstream received the full 66-byte payload past the 32-byte cap — authorization bypass.
(Full PoC source is appended below by the submission tool via --poc-file.)
Adversarial re-read (refutation attempts)
- "Truncated JSON just fails to parse → fail closed." True for
application/jsonwhen truncation lands mid-token, but the bypass does not rely on JSON:application/x-www-form-urlencodedparses leniently after truncation, and unparsed/absent content-types return(nil, false, nil)with no error. The load
Références
- https://github.com/advisories/GHSA-5gpm-rgj3-9q76
- https://github.com/zalando/skipper/security/advisories/GHSA-5gpm-rgj3-9q76
- https://nvd.nist.gov/vuln/detail/CVE-2026-86043
- https://github.com/zalando/skipper/commit/2cfceabaa6ff0af65b312dcb9bcbe84691b9d507
- https://github.com/zalando/skipper/releases/tag/v0.27.35
- https://github.com/zalando/skipper/releases/tag/v0.27.37
- https://github.com/zalando/skipper/tree/v0.27.35
Vulnérabilités liées
Tout Supply chain →- MEDIUMCVE-2026-96628
Arcane: Environment access token bypasses the admin-required authorization gate on `/api/debug/pprof`
- MEDIUMCVE-2026-58494
Wasmtime: WASI hard links bypass wasmtime-wasi's FilePerms for destination
- MEDIUMCVE-2026-102832
JupyterLab: Command link trust prompt in JupyterLab can trust a document other than the one clicked
- MEDIUMCVE-2026-103028
SFTPGo source-side file-pattern filter bypass during copy operations
- CRITICALCVE-2026-77459
Argo CD: AppProject destination-namespace and resource restrictions bypassed by PreDelete/PostDelete resource hooks
- MEDIUMGHSA-fmmf-xq98-g327
Vikunja: Write-level project members can delete admin-tier link shares through an unloaded permission check