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:
- 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.
- 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 leak —
libdeflate_alloc_decompressor()was not freed before a retry allocation, leaking the handle on every retry attempt. - Single retry only — A
bool retriedflag 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\nsequences. An attacker-controlled response could inject arbitrary HTTP headers into the forwarded stream. - HTTP request smuggling —
Transfer-EncodingandContent-Lengthheaders 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.binfile 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 returnedfalse— The cookieSecureattribute was hardcoded to returnfalseregardless of the actual cookie state, causingSecurecookies 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 HTTP429 Too Many Requestsresponse before being drained. - The quota is reset when an IP is pardoned via
firewall -unbanorfirewall -clear. - The quota is visible in real time via
firewall -listandnet -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