bmcweb HTTP/2: unbounded request body accumulation before authentication
binreaper <[email protected]>
| Newsgroups | org.ozlabs.lists.openbmc |
|---|---|
| Message-ID | <jpYy_rQWOjeJJrLVapYy1VuFk7D3IeFfC6aFviq_lZz6FbXJTvPA8uPne4C1hqR9jYFTs-PFd2s16vpqj_4dCKz3JLwh4__9beqsHH42K_k=@proton.me> |
Hi all, Posting to the public list per Ed Tanous's 2026-05-14 instruction that further correspondence on the bmcweb security findings I reported in February should go through public OpenBMC channels. This is a public follow-up on one of the four findings I sent to [email protected] on 2026-02-23. It was acknowledged by Joseph Reynolds, forwarded to the bmcweb maintainers, and bundled in the same email as the H2 reserve-crash issue (which has since been patched on master as commit `62526bb0`). The bundling appears to have been a mistake on my part — this finding is a separate, more severe bug and did not receive a Gerrit change or a fix on master. Posting it here now so it's on the record and any downstream consumer can patch. A reproducer outline is included below; the reproducer itself is held back from the list per `openbmc-security` guidance and is available on request. A suggested Gerrit change will follow under separate cover. ## Summary bmcweb's HTTP/2 request path accepts unlimited request body data from unauthenticated clients before authentication runs. The HTTP/1.1 path enforces a 4 KB unauthenticated body limit via Boost.Beast's parser; the HTTP/2 path has no equivalent enforcement. An unauthenticated client can open an h2 connection, send a `POST`, and stream DATA frames until the BMC's single-threaded event loop dies from out-of-memory. - **Affected versions**: bmcweb on master as of 2026-05-14, all earlier versions with HTTP/2 enabled. - **Configuration prerequisite**: `BMCWEB_EXPERIMENTAL_HTTP2` (or its current equivalent) enabled at build time. HTTP/2 is the default protocol path via ALPN negotiation when enabled. - **Severity**: I scored this CVSS 7.5 (`AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H`), matching the severity Ed assigned to the related HTTP/1.1 `Expect: 100-continue` issue published as GHSA-p3gc-68x5-g9w3. This finding is in the same severity band and shares a root cause family. ## Code path The HTTP/1.1 path inherits its body limit from Boost.Beast's parser, which is set per-request via `parse.body_limit(getContentLengthLimit())` in `http/http_connection.hpp`. The HTTP/2 path does not use Boost.Beast for body assembly — it uses `HttpBody::reader` directly (defined in `http/http_body.hpp`). The relevant method on the reader is `put()`. Looking at its current implementation on master (verified against commit `6a82fead`, 2026-05-19): ``` // http/http_body.hpp, around line 440 template <class ConstBufferSequence> std::size_t put(const ConstBufferSequence& buffers, boost::system::error_code& ec) { size_t extra = boost::beast::buffer_bytes(buffers); for (const auto b : boost::beast::buffers_range_ref(buffers)) { const char* ptr = static_cast<const char*>(b.data()); if (multipartParser) { /* parse-only, no size check */ } else { value.str().append(ptr, b.size()); } } ec = {}; return extra; } ``` `put()` appends incoming bytes to `value.str()` (a `std::string`) with **no size check whatsoever**. There is no body-limit field on the reader, no per-stream byte counter that's compared against a maximum, and no early-rejection path. The function trusts the caller to bound the total. The multipart branch is also unbounded — it parses but doesn't reject on accumulated size. The caller in the HTTP/2 path is the nghttp2 DATA-frame callback in `http/http2_connection.hpp` (the `onDataChunkRecvCallback` family). That callback forwards each chunk to `put()`. nghttp2 auto-replenishes flow-control windows by default; bmcweb does not set `nghttp2_option_set_no_auto_window_update`, so there is no flow-control backpressure to slow a hostile client. Authentication runs in `onRequestRecv`, which nghttp2 invokes on END_STREAM — i.e., after the full body has been received and buffered. This means the entire body is accumulated in the `HttpBody::reader`'s `std::string` *before* `onRequestRecv` ever decides whether the request was authenticated. The result is a pre-authentication memory-exhaustion primitive over HTTP/2. ## Reproducer outline The reproducer is a short Python script using `h2` (the `python-hyper/h2` package) and a raw TLS socket. It: 1. Performs the TLS handshake with ALPN `h2`. 2. Sends the HTTP/2 connection preface and SETTINGS. 3. Opens a single stream with `:method POST`, `:path /redfish/v1/SessionService/Sessions`, `Content-Type: application/json`, and a body length declared via END_HEADERS (no Content-Length required for HTTP/2 streamed bodies). 4. Sends DATA frames of 16 KB each in a loop, never setting END_STREAM. 5. Calls `nghttp2_session_consume_*` equivalents on the receiver side via flow-control updates as needed (nghttp2 server-side replenishes automatically; client only needs to keep streaming). Against a vanilla bmcweb build with HTTP/2 enabled, the BMC's RSS climbs linearly with bytes sent. Death threshold depends on platform RAM: - 256 MB BMC: dies around ~200 MB sent. - 1 GB BMC: dies around ~800 MB sent. The session does not need to be authenticated. The BMC does not return any response until END_STREAM, which the attacker never sends. There is no rate-limiting between DATA frames at the bmcweb layer; nghttp2's flow-control windows are auto-replenished, so the only bound is the attacker's network speed. I will not attach the reproducer to this list post (per `openbmc-security` guidance about not posting exploitation tooling to the public list). It is available on request to anyone with a legitimate downstream-vendor or fork-maintainer interest. I plan to publish the reproducer alongside a writeup once a fix is in master. ## Suggested fix Two options, in increasing order of robustness: **Option 1 — Per-stream byte counter on `HttpBody::reader`:** Add a `size_t maxSize` field to the reader, initialized to a sensible default (the equivalent of `BMCWEB_HTTP_BODY_LIMIT`, or 4 KB for the unauthenticated case if the auth state is available at reader construction). In `put()`, accumulate `bytesReceived += buffer.size()` and return an error if `bytesReceived > maxSize`. The nghttp2 callback can translate the error into a `RST_STREAM` with `INTERNAL_ERROR` or `ENHANCE_YOUR_CALM`. This mirrors what Boost.Beast's parser does internally for the HTTP/1.1 path and is the minimum-change fix. **Option 2 — Pre-auth hard cap independent of `Content-Length`:** Set a 4 KB hard cap at the start of every HTTP/2 stream, before authentication runs. Once `onRequestRecv` validates a session, the cap is raised to the authenticated limit (`BMCWEB_HTTP_BODY_LIMIT`) and any already-received bytes are checked against the new cap. This requires the reader to be aware of the authenticated/unauthenticated state, which is cleaner if done at the stream layer rather than via the reader's `put()`. Option 1 is the smaller patch and unblocks the immediate issue. Option 2 is closer to how the HTTP/1.1 path actually works after the `Expect: 100-continue` fix. A separate, related concern: the HTTP/2 path has no overall connection deadline timer comparable to the HTTP/1.1 15-second request timeout (`http/http_connection.hpp`). A hostile client can complete the TLS handshake and the h2 preface, then idle the connection indefinitely. This isn't part of H2-F1, but a fix for H2-F1 that doesn't also address the lack of an idle/deadline timer leaves a Slowloris-style primitive in place. I'll send a separate post about this if useful. ## Timeline - **2026-02-23** — Reported privately to `[email protected]`, bundled with the H2 reserve-crash issue under a single email subject. Acknowledged the same day by Joseph Reynolds, forwarded to the bmcweb maintainers. - **2026-02-26** — Gerrit change uploaded by Gunnar Mills addressing the *reserve-crash* portion of the bundled email (`http/http_body.hpp::init()`, Content-Length guard). The body-accumulation portion — this finding — did not receive a Gerrit change. - **2026-04-21 17:31 UTC** — The reserve-crash patch merged to master as commit `62526bb0`. The body-accumulation issue remained unfixed. - **2026-05-05** — Status-check email to the maintainers. No reply. - **2026-05-14** — Maintainer reply (Ed Tanous): the publicly disclosed sibling bug is "no longer embargoed, and the fix is on master. There's no longer a reason to send direct messages to the security responders. If you believe further action is needed, please use the normal project communication channels (mailing list, discord, gerrit reviews)." This post is that follow-up. ## Acknowledgements and intent Thanks to Joseph Reynolds for the initial triage and routing, and to Gunnar Mills and Ed Tanous for the fixes that did land on the related issues. My intent in posting publicly is not to escalate or pressure; it is to honor Ed's instruction to use the public channels while ensuring that downstream consumers — particularly BMC firmware vendors who track upstream bmcweb without independently auditing the HTTP/2 path — have the information they need to patch. I'm happy to draft and submit the Gerrit change myself if it would help; let me know. Best regards, binreaper [email protected]