GRIDNET Core Security Bulletin — March 2026

Reverse CORS Proxy Hardening and Attack Mitigations


Background

Over the past week, GRIDNET full-node operators running the reverse CORS proxy (the built-in web forwarding service that allows decentralised applications to reach external web resources without browser-side CORS restrictions) observed a sustained campaign of abuse attempts directed at that subsystem.

The attacks fell into two broad patterns:

  1. Crash exploitation — Sending very large HTTP POST or PUT requests (over 1 MB) to trigger a memory-access violation inside the proxy’s body-parsing path.
  2. Decompression amplification — Sending streams of heavily compressed payloads (gzip / deflate / Brotli) designed to cause the node to allocate disproportionate amounts of memory during decompression, and to exhaust the decompressor’s retry budget via deliberately malformed content.

Several crash dumps were received from Tier 1 operator nodes (1.9.7 release line). Analysis confirmed both attack patterns were actively exploited. All issues have been resolved and the fixes are included in r5097 → r5104.


What Was Fixed

1. Large-Body Crash in the Proxy Ingress Path (r5097)

Severity: Critical

The Mongoose HTTP event model fires two distinct events when receiving HTTP data: MG_EV_HTTP_MSG (full message received) and MG_EV_HTTP_CHUNK (partial data received). The proxy’s ingress handler was calling forward_request() on both events. For requests with a body larger than the current TCP receive buffer, Mongoose fires MG_EV_HTTP_CHUNK first — at which point hm->body.len reports the full Content-Length from the header (e.g. 1.2 MB) while only a fraction of that data has actually arrived in the buffer.

Constructing a std::string from a pointer and a length that extends beyond the received data caused an ACCESS_VIOLATION (confirmed in crash dumps from multiple operator nodes). A secondary bug in the same callback — an assignment-instead-of-comparison typo (ev = MG_EV_READ rather than ev == MG_EV_READ) — was also clearing the server receive buffer on every Mongoose event, not just read events.

Fix: The proxy now skips forward_request() when the event is MG_EV_HTTP_CHUNK and the method is POST or PUT with a non-zero body. Processing is deferred until MG_EV_HTTP_MSG delivers the complete body. The assignment typo was corrected.


2. Decompression Buffer Hardening (r5100)

Severity: High

The CTools::decodeWebString() decompression utility had several weaknesses that the amplification attack exposed:

  • Memory leaklibdeflate_alloc_decompressor() was not freed before a retry allocation, leaking the handle on every retry attempt.
  • Single retry only — A bool retried flag prevented a second retry even when a larger buffer would have succeeded. Payloads that genuinely required more than one size doubling silently fell through.
  • No deflate retry — The deflate codec path had no retry logic at all; any insufficiently-sized initial buffer produced an immediate, silent failure.
  • No zip-bomb cap — The output buffer grew 4× on each retry with no upper bound, meaning a pathological payload could trigger unbounded memory allocation.

Fix: A hard cap of 16 MB on decompressed output was introduced. The retry loop was restructured to allow multiple attempts (up to the cap). The memory leak was plugged. The deflate path now has parity with the gzip and Brotli paths.


3. Security Audit — CORS Proxy Surface (r5101 / r5102)

Severity: High / Medium

A full security audit of the reverse CORS proxy implementation surfaced six additional issues corrected in this release:

  • CRLF injection (two sites) — Untrusted header names and values forwarded to the endpoint, and the host header in HTTPS redirect responses, were not stripped of \r\n sequences. An attacker-controlled response could inject arbitrary HTTP headers into the forwarded stream.
  • HTTP request smugglingTransfer-Encoding and Content-Length headers supplied by the client were forwarded verbatim to the upstream endpoint. These are now unconditionally blocked in the outbound header list.
  • Debug artefact — A lastResponse.bin file written to disk on every response was removed.
  • Connection ID vs. pointer confusion — An accepted-connection lookup was comparing a loop index against a connection ID, silently skipping the correct entry and returning false positives.
  • Missing mutex in cleanPerRequestFlags() — The per-request flag reset was not guarded, allowing a race between the cleanup path and concurrent event handlers.
  • getIsSecure() always returned false — The cookie Secure attribute was hardcoded to return false regardless of the actual cookie state, causing Secure cookies to be sent over plain HTTP during proxy-assisted sessions.

4. Per-IP Hourly Decompression Quota (r5103 / r5104)

Severity: Medium (Denial of Service mitigation)

Even with the buffer cap in place, a single remote IP could issue repeated decompression requests — each within the 16 MB individual cap — and sustain a continuous high-CPU / high-memory load on the node. No per-connection accounting existed.

A new per-IP decompression quota is now enforced directly inside the existing CNetworkManager firewall infrastructure:

  • Default limit: 25 MB of decompressed data per IP per hour (same 3600-second sliding window used by all other firewall counters).
  • Both successful and failed decompressions count against the quota. A failed attempt charges the compressed input size (a proxy for the CPU already spent attempting decompression). This closes the attack vector where malformed payloads consumed server CPU without advancing the attacker’s counter.
  • When the quota is exceeded, the IP is banned via the standard banIP() path (kernel-mode firewall integration when available), and the current connection receives an HTTP 429 Too Many Requests response before being drained.
  • The quota is reset when an IP is pardoned via firewall -unban or firewall -clear.
  • The quota is visible in real time via firewall -list and net -connections, both of which now include a “Proxy MB Left” column (highlighted red when below 25% remaining).

A post-implementation code review identified and corrected a lock-order inversion (potential deadlock between mDecompressionBytesGuardian and mBannedIPsGuardian), a uint64_t overflow bypass in the byte accumulator, and a missing IP-format validation call — all corrected in r5104 before release.


Operator Guidance

Update to 1.9.7 (r5104 or later). All operators running the reverse CORS proxy service (enabled by default on full nodes with web networking active) should update at the earliest opportunity.

No configuration changes are required. The 25 MB/hour decompression quota is active by default. To review current per-IP decompression usage on a running node:

firewall -list

or for the full connection report including decompression quota:

net -connections

To manually ban an IP that is being abusive:

firewall -ban <IP>

To clear all firewall state and decompression counters:

firewall -clear all

Revision Summary

Revision Change
r5097 Fixed large-body ACCESS_VIOLATION crash in proxy ingress; fixed ev = MG_EV_READ typo clearing server recv buffer
r5098 SVN commit of r5097 artifacts
r5100 Decompression buffer hardening: 16 MB cap, memory leak fix, deflate retry, multiple retries
r5101 Security audit fixes — CORSProxy.cpp: CRLF injection (×2), debug file removal, HTTP smuggling headers, connection ID bug, mutex on flag reset
r5102 Security audit fixes — Tools.cpp: compress_bound fix, log level correction; proxyConnection.cpp: getIsSecure()
r5103 Per-IP hourly decompression quota (25 MB/hr); firewall/net command reporting; NetworkManager infrastructure
r5104 Post-review hardening: deadlock fix, saturating arithmetic, IP validation, failed-decompression accounting, man-page updates

GRIDNET Core Development Team — March 2026