Skip to content

Cap curl HTTP response body size to prevent memory-amplification DoS#1507

Open
bmehta001 wants to merge 3 commits into
microsoft:mainfrom
bmehta001:bhamehta/cap-http-response-size
Open

Cap curl HTTP response body size to prevent memory-amplification DoS#1507
bmehta001 wants to merge 3 commits into
microsoft:mainfrom
bmehta001:bhamehta/cap-http-response-size

Conversation

@bmehta001

Copy link
Copy Markdown
Contributor

Summary

The libcurl HTTP transport buffered the entire collector response with no
upper bound. Both response-buffering callbacks grew until the whole body was
received:

  • WriteMemoryCallbackrealloc(mem->memory, mem->size + realsize + 1) then append (raw-response path)
  • WriteVectorCallbackdata->insert(data->end(), begin, end) (normal path)

A hostile or MITM'd collector (reachable e.g. via a redirected/attacker-controlled
SetCollectorUri) could return an arbitrarily large body and drive unbounded
memory growth
in the embedding process — a memory-amplification denial of
service. Legitimate OneCollector responses are tiny (status line, kill-switch
tokens, Retry-After, small config), so a generous fixed cap never rejects a
real response.

Change

  • Add a 16 MB cap kMaxResponseBytes and enforce it overflow-safely
    (realsize > kMaxResponseBytes - buffered) in both curl write callbacks.
  • Exceeding the cap returns a short byte count, which makes libcurl abort the
    transfer with CURLE_WRITE_ERROR. The SDK already maps that to
    HttpResult_NetworkFailure, so the upload is treated as a transient network
    failure and retried — no crash, no partial-response corruption.

Tests

New HttpClientCurlResponseCapTests (activates on the curl path: Linux,
non-Apple, non-Android) drive a real in-process HttpServer:

  • AbortsOversizedResponseBody — a 17 MB body is refused: result is
    NetworkFailure and the buffered body never exceeds the cap.
  • AcceptsLargeResponseUnderCap — a 4 MB body is still received in full
    (HttpResult_OK, status 200, exact size), proving the cap does not regress
    normal responses.

Validation (WSL, GCC 13, -std=c++11, Debug):

  • UnitTests build clean.
  • HttpClientCurl*Tests.* — 13/13 pass (incl. the 2 new).
  • Http* / *Curl* suites — 38/38 pass (no regression).

Scope / follow-ups

  • This caps the libcurl transport, which is the primary threat-model
    transport on Linux/Android/POSIX. The unbounded-buffering pattern also exists
    per-transport in WinInet (m_bodyBuffer), WinRt, and Apple NSURLSession;
    capping those is a reasonable follow-up for full coverage.
  • The cap is a conservative fixed constant. Making it configurable via
    ILogConfiguration (e.g. a CFG_INT_HTTP_MAX_RESPONSE_SIZE key) could be
    added later if any consumer legitimately needs a larger ceiling.

The libcurl transport buffered the entire collector response with no upper
bound: WriteMemoryCallback and WriteVectorCallback grew (realloc / vector
insert) until the whole body was received. A hostile or MITM'd collector
could return an arbitrarily large body and drive unbounded memory growth in
the embedding process. OneCollector responses are tiny (status, kill-switch
tokens, retry-after, small config), so a generous fixed cap never rejects a
legitimate response.

Add a 16 MB cap (kMaxResponseBytes) enforced overflow-safely in both curl
write callbacks; exceeding it returns a short count so curl aborts the
transfer with CURLE_WRITE_ERROR (the upload becomes a network failure and is
retried). Add HttpClientCurlResponseCapTests covering the oversized-abort and
large-under-cap paths.

Files:
- lib/http/HttpClient_Curl.hpp
- tests/unittests/HttpClientCurlTests.cpp

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@bmehta001
bmehta001 requested a review from a team as a code owner July 15, 2026 01:17
@bmehta001
bmehta001 requested a review from Copilot July 15, 2026 01:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the libcurl HTTP transport against memory-amplification DoS by enforcing a fixed upper bound on buffered HTTP response bodies, and adds unit tests that validate both aborting oversized responses and accepting large responses under the cap.

Changes:

  • Introduces a 16 MB response-body buffering cap (kMaxResponseBytes) in libcurl write callbacks.
  • Aborts transfers that exceed the cap so oversized responses are treated as network failures.
  • Adds new in-process HttpServer-backed unit tests validating both oversized and under-cap responses on the curl path.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
tests/unittests/HttpClientCurlTests.cpp Adds new curl-only tests using an in-process HTTP server to validate response-body cap behavior.
lib/http/HttpClient_Curl.hpp Adds a 16 MB buffered-response cap and enforces it in both curl write callbacks.
Comments suppressed due to low confidence (1)

lib/http/HttpClient_Curl.hpp:566

  • WriteVectorCallback (1) calculates realsize = size * nmemb without checking for multiplication overflow and (2) returns size * nmemb at the end instead of returning the already-computed realsize. Both can lead to incorrect return values (and potentially bypass/disable the intended cap logic if overflow occurs), and also recompute the potentially-overflowing expression.
    static size_t WriteVectorCallback(void *ptr, size_t size, size_t nmemb, std::vector<uint8_t>* data)
    {
        if (data != nullptr) {
            size_t realsize = size * nmemb;
            // SECURITY: bound the buffered response (see kMaxResponseBytes). Compare
            // overflow-safely (data->size() is always <= kMaxResponseBytes here).
            // Returning a short count aborts the transfer with CURLE_WRITE_ERROR.
            if (realsize > kMaxResponseBytes - data->size()) {
                TRACE("Response exceeds max buffered size (%zu bytes); aborting transfer\n", kMaxResponseBytes);
                return 0;
            }
            const auto* begin = static_cast<const uint8_t*>(ptr);
            const auto* end   = begin + realsize;
            data->insert( data->end(), begin, end);
        }
        return size * nmemb;
    }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/http/HttpClient_Curl.hpp
Comment thread tests/unittests/HttpClientCurlTests.cpp
Comment thread tests/unittests/HttpClientCurlTests.cpp
Comment thread tests/unittests/HttpClientCurlTests.cpp
…und 1)

Address Copilot review on the response-size cap:
- Guard the size * nmemb product in both curl write callbacks against size_t
  multiplication overflow before using it, so the "overflow-safe" cap check
  cannot operate on a wrapped length.
- The response-cap test fixture now owns the IHttpRequest (the client only
  stores a raw pointer and never frees it) and releases it in TearDown on the
  main thread, fixing the per-test leak. Freeing it in OnHttpResponse would
  destroy the CurlHttpOperation from within its own async task (whose
  destructor waits on that task -- a self-join deadlock), so teardown-time
  release is used instead. sendAndWait() also resets result state up front so
  the helper is safe to reuse.

Files:
- lib/http/HttpClient_Curl.hpp
- tests/unittests/HttpClientCurlTests.cpp

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment thread tests/unittests/HttpClientCurlTests.cpp
Comment thread tests/unittests/HttpClientCurlTests.cpp
Address Copilot review round 2: m_responseBodySize was written by the test
thread in sendAndWait() and read by the HttpServer reactor thread in
onHttpRequest() without synchronization -- a data race that would trip TSAN.
Write it under the existing mutex and read it under the same lock (into a
local) to establish a happens-before edge.

Files:
- tests/unittests/HttpClientCurlTests.cpp

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@bmehta001 bmehta001 self-assigned this Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants