Skip to content

Commit b76c4cf

Browse files
bryancallclaude
andcommitted
Fix H2 IWS=0 and HTTP/1 slow-read response-body buffering DoS.
HTTP/2 INITIAL_WINDOW_SIZE=0 attack: a client advertising a zero send window causes ATS to fetch origin responses that it cannot forward, allowing unbounded response-body accumulation in IOBuffers. For HTTP/2, add a per-stream _send_buffer_full flag. When send_a_data_frame() returns NO_WINDOW, the flag is set and update_write_request() skips future send attempts, preventing the origin read VIO from being re-enabled. The flag is cleared in restart_sending() when a WINDOW_UPDATE opens the peer window. Measured buffer per stream: ~86 KB vs 100 MB origin body. For HTTP/1, enable HttpTunnel flow control by default (high_water=32 MB, low_water=8 MB). TCP backpressure already naturally limits per-stream buffering to ~150 KB, and the application-level watermarks add a defense-in-depth cap. Measured buffer per stream: ~148 KB vs 100 MB. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 6f2dfda commit b76c4cf

4 files changed

Lines changed: 23 additions & 3 deletions

File tree

include/proxy/http2/Http2Stream.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ class Http2Stream : public ProxyTransaction
189189

190190
bool parsing_header_done = false;
191191
bool is_first_transaction_flag = false;
192+
bool _send_buffer_full = false;
192193

193194
HTTPHdr _send_header;
194195
IOBufferReader *_send_reader = nullptr;

src/proxy/http2/Http2ConnectionState.cc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2298,6 +2298,13 @@ Http2ConnectionState::send_a_data_frame(Http2Stream *stream, size_t &payload_len
22982298
}
22992299
Http2StreamDebug(this->session, stream->get_id(), "No window session_wnd=%zd stream_wnd=%zd peer_initial_window=%u",
23002300
get_peer_rwnd(), stream->get_peer_rwnd(), this->peer_settings.get(HTTP2_SETTINGS_INITIAL_WINDOW_SIZE));
2301+
2302+
// Mark the stream write-stalled so update_write_request skips future
2303+
// send attempts until restart_sending clears this on a WINDOW_UPDATE.
2304+
// This stops the origin read VIO from being re-enabled while the peer
2305+
// window is zero, bounding how much response body can accumulate.
2306+
stream->_send_buffer_full = true;
2307+
23012308
this->session->flush();
23022309
return Http2SendDataFrameResult::NO_WINDOW;
23032310
}

src/proxy/http2/Http2Stream.cc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,11 @@ Http2Stream::restart_sending()
772772
if (this->is_closed()) {
773773
return;
774774
}
775+
776+
// The peer's flow-control window has opened; allow update_write_request to
777+
// proceed again so origin reads can resume.
778+
_send_buffer_full = false;
779+
775780
if (!this->parsing_header_done) {
776781
this->update_write_request(true);
777782
return;
@@ -806,6 +811,13 @@ Http2Stream::update_write_request(bool call_update)
806811
return;
807812
}
808813

814+
// If we are backed up waiting for the peer's flow-control window to open,
815+
// don't consume more data from the origin-side buffer. This prevents the
816+
// origin read VIO from being re-enabled and keeps memory bounded.
817+
if (parsing_header_done && _send_buffer_full) {
818+
return;
819+
}
820+
809821
if (!this->_switch_thread_if_not_on_right_thread(VC_EVENT_WRITE_READY, nullptr)) {
810822
// Not on the right thread
811823
return;

src/records/RecordsConfig.cc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -349,11 +349,11 @@ static constexpr RecordElement RecordsConfig[] =
349349
,
350350
{RECT_CONFIG, "proxy.config.http.strict_chunk_parsing", RECD_INT, "1", RECU_DYNAMIC, RR_NULL, RECC_INT, "[0-1]", RECA_NULL}
351351
,
352-
{RECT_CONFIG, "proxy.config.http.flow_control.enabled", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
352+
{RECT_CONFIG, "proxy.config.http.flow_control.enabled", RECD_INT, "1", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
353353
,
354-
{RECT_CONFIG, "proxy.config.http.flow_control.high_water", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
354+
{RECT_CONFIG, "proxy.config.http.flow_control.high_water", RECD_INT, "33554432", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
355355
,
356-
{RECT_CONFIG, "proxy.config.http.flow_control.low_water", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
356+
{RECT_CONFIG, "proxy.config.http.flow_control.low_water", RECD_INT, "8388608", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
357357
,
358358
{RECT_CONFIG, "proxy.config.http.post.check.content_length.enabled", RECD_INT, "1", RECU_DYNAMIC, RR_NULL, RECC_INT, "[0-1]", RECA_NULL}
359359
,

0 commit comments

Comments
 (0)