http: do not send a body in response to HEAD requests #36354

pull azuchi wants to merge 1 commits into bitcoin:master from azuchi:http-head-no-body changing 3 files +75 −1
  1. azuchi commented at 2:49 AM on September 27, 2026: contributor

    HTTPRequest::WriteReply() sends the reply body regardless of the request method, so a HEAD request is answered with the same body as the equivalent GET. RFC 9110 section 9.3.2 requires the HEAD response to carry the header section of the GET response (so Content-Length describes the body GET would have returned) but no body.

    $ printf 'HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n' | nc 127.0.0.1 18443
    HTTP/1.1 405 Method Not Allowed
    Date: ...
    Content-Length: 41
    Content-Type: text/html; charset=ISO-8859-1
    
    JSONRPC server handles only POST requests
    

    A client that follows the specification stops reading after the header section, so the body bytes stay on the connection and are read as the start of the next response. On a direct connection this only confuses the client that sent the HEAD. With a reverse proxy that keeps upstream connections alive (for example nginx with proxy_http_version 1.1 and an upstream { keepalive N; } block) the stray bytes end up in the proxy's connection pool and corrupt the next response to whichever client is served from that connection. Both the 405 reply of the RPC endpoint and the REST endpoints answer HEAD without authentication, so a client that is merely allowed to connect can trigger it. The body is not attacker controlled, so this is a robustness issue rather than response injection.

    This is not a regression from #35182: the libevent based server behaved the same way. evhttp_send_reply() writes the output buffer regardless of the method, and evhttp_response_needs_body() only suppresses the Content-Length and Content-Type headers for HEAD (so the old server sent the body without a Content-Length). The needs_body flag in WriteReply() was ported with that same role, which is why this PR leaves it alone and only drops the body: the header section stays identical to the GET response, as the RFC recommends.

    The functional tests cover the RPC endpoint (405 path) and a REST endpoint (200 path). They send the HEAD request over a raw socket and read until the server goes quiet, checking that Content-Length equals the GET body length and that no bytes follow the header section. http.client cannot be used for this check because it silently discards a wrongly attached body.

  2. DrahtBot added the label RPC/REST/ZMQ on Sep 27, 2026
  3. DrahtBot commented at 2:49 AM on September 27, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/36354.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process. A summary of reviews will appear here.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  4. http: do not send a body in response to HEAD requests
    WriteReply() sent the reply body regardless of the request method, so
    HEAD requests were answered with the same body as GET. RFC 9110
    section 9.3.2 requires a HEAD response to carry the header section of
    the GET response, Content-Length included, but no body.
    
    A client stops reading after the header section, so the body bytes are
    left on the connection and are read as the start of the next response.
    With a reverse proxy that keeps upstream connections alive, this
    corrupts the next response served from that connection. The RPC and
    REST endpoints answer HEAD without authentication.
    
    The libevent based server had the same behaviour, only without
    Content-Length: evhttp_response_needs_body() controls the headers, not
    whether the body is written. needs_body keeps that role here.
    5548698251
  5. azuchi force-pushed on Sep 27, 2026
  6. DrahtBot added the label CI failed on Sep 27, 2026
  7. DrahtBot removed the label CI failed on Sep 27, 2026

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-09-28 10:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me