Skip to main content
CVE Vulnerability Database
Vulnerability Database/CVE-2026-89186

CVE-2026-89186: ZenHive MPP Information Disclosure Flaw

CVE-2026-89186 is an information disclosure vulnerability in ZenHive MPP that allows HTTP caches to serve paid content to unpaid clients. This article covers the technical details, affected versions, and mitigation strategies.

Published:

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.

text
// 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.
bash
# 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.

Default Legacy - Prefooter | Experience the World’s Most Advanced Cybersecurity Platform

Experience the Most Advanced Cybersecurity Platform

See how the world’s most intelligent, autonomous cybersecurity platform can protect your organization today and into the future.