CVE-2026-89186 Overview
CVE-2026-89186 is a sensitive information caching flaw [CWE-524] in ZenHive mpp, an Elixir library that implements payment-gated HTTP responses. The MPP.Plug.verify_credential function in lib/mpp/plug.ex sets the Payment-Receipt header and Cache-Control: private on the connection before the wrapped application runs. Because Plug.Conn.put_resp_header/3 replaces existing headers, any downstream application that later sets its own Cache-Control (for example public, max-age=3600) silently overrides the library's private directive. A CDN or reverse proxy can then cache the paid 200 response together with its Payment-Receipt and serve both to clients that never paid.
Critical Impact
Paid, receipt-bearing HTTP responses may be cached by shared proxies and served to unauthenticated clients, defeating the library's payment enforcement guarantee.
Affected Products
- ZenHive mpp versions 0.1.0 through 0.16.1
- Applications embedding MPP.Plug.verify_credential from lib/mpp/plug.ex
- JSON-RPC over HTTP transports using lib/mpp/transports/json_rpc/plug.ex
Discovery Timeline
- 2026-09-16 - CVE-2026-89186 published to NVD
- 2026-09-16 - Last updated in NVD database
Technical Details for CVE-2026-89186
Vulnerability Analysis
The root defect is an ordering and precedence issue in Plug header composition. MPP.Plug.verify_credential writes Cache-Control: private and Payment-Receipt onto the connection before invoking the wrapped application. It does not use Plug.Conn.register_before_send/2, so those headers are treated as regular response headers rather than as a final-stage guarantee. When the downstream application calls put_resp_header("cache-control", "public, max-age=3600") on a paid resource, the library's private directive is replaced. Shared caches then treat the response as publicly cacheable while it still carries the Payment-Receipt header.
A second consequence of the same design is that non-2xx responses from the downstream application still carry Payment-Receipt, effectively issuing a receipt for content that was never delivered.
Root Cause
The library conflates pre-dispatch header assignment with a post-dispatch guarantee. Header values set before plug chain dispatch are mutable by downstream handlers. Cache-sensitive headers required to enforce a security property must be applied in a register_before_send/2 callback so they are written after downstream logic runs.
Attack Vector
An unauthenticated network attacker requests a payment-gated URL that a paying client has recently fetched through a shared CDN or reverse proxy. If the origin application set a public Cache-Control on the paid resource, the CDN serves the cached 200 response, including the paying client's Payment-Receipt, to the attacker without any payment credential.
// Patch excerpt: lib/mpp/transports/json_rpc/plug.ex
defp send_json(conn, status, body) do
conn
+ |> register_response_headers(body)
|> Plug.Conn.put_resp_content_type("application/json")
- |> Plug.Conn.put_resp_header("cache-control", "private, no-store")
|> Plug.Conn.send_resp(status, Jason.encode!(body))
|> Plug.Conn.halt()
end
+defp register_response_headers(conn, body) do
+ receipt = get_in(body, ["_meta", JsonRpc.receipt_meta_key()])
+
+ Plug.Conn.register_before_send(conn, fn conn ->
+ if conn.status in 200..299 and is_map(receipt) do
+ conn
+ |> Plug.Conn.put_resp_header("payment-receipt", encode_receipt(receipt))
+ |> MPP.Plug.put_private_cache_control()
+ else
+ Plug.Conn.put_resp_header(conn, "cache-control", "no-store")
+ end
+ end)
+end
Source: GitHub commit 2fd91a5. The patch moves receipt and cache-control emission into a register_before_send/2 callback so downstream headers cannot override the private directive, and only issues a Payment-Receipt when the final status is 2xx.
Detection Methods for CVE-2026-89186
Indicators of Compromise
- HTTP responses containing a Payment-Receipt header alongside a Cache-Control value that permits shared caching, such as public, s-maxage, or a non-zero max-age without private or no-store.
- CDN or reverse-proxy cache entries with HIT status for URLs protected by MPP.Plug.verify_credential.
- Non-2xx origin responses (for example 404 or 500) that still include a Payment-Receipt header.
Detection Strategies
- Inspect proxy and CDN access logs for cache HIT events on routes wrapped by MPP.Plug.
- Run a synthetic client that requests a paid endpoint without an Authorization: Payment header and alert if a 200 response with a Payment-Receipt is returned.
- Statically scan Elixir application code for put_resp_header("cache-control", ...) calls on routes mounted behind MPP.Plug.verify_credential.
Monitoring Recommendations
- Emit metrics on the ratio of Payment-Receipt responses to authenticated payment requests; a divergence indicates cache reuse.
- Log and review any response where Cache-Control lacks both private and no-store on mpp-protected paths.
- Enable CDN header logging to capture Cache-Control, Vary, and Age values for post-incident analysis.
How to Mitigate CVE-2026-89186
Immediate Actions Required
- Upgrade ZenHive mpp to version 0.16.2 or later.
- Purge shared CDN and reverse-proxy caches for any URL previously served through MPP.Plug.verify_credential.
- Audit downstream Plug pipelines and remove any application-level Cache-Control header that permits shared caching on paid resources.
Patch Information
The fix is delivered in mpp0.16.2. Relevant commits are 2d4d1d9 and 2fd91a5. See the GitHub Security Advisory GHSA-82qh-vrvm-gqvc and the Erlang Ecosystem Foundation CNA advisory for full details. The patch moves Payment-Receipt and Cache-Control: private emission into a register_before_send/2 callback and gates receipt issuance on a 2xx status.
Workarounds
- Configure the upstream CDN or reverse proxy to force Cache-Control: private, no-store on all routes protected by MPP.Plug.
- Add an application-level register_before_send/2 callback that rewrites any non-private Cache-Control to private, no-store when a Payment-Receipt header is present.
- Strip Payment-Receipt headers from any response whose status is outside the 200..299 range.
# Update mix.exs and fetch the patched release
mix deps.update mpp
mix deps.get
# Verify the installed version is >= 0.16.2
mix deps | grep mpp
Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.

