Skip to content

Commit 34a804d

Browse files
Re-sync with internal repository with CVE-2023-44487 changes (#466)
Original message: > I'm modifying the ControlMessageRateLimitFilter in order to rate limit headers. When we receive too many headers in an interval, we call onGoaway instead of propagating the headers down the filter chain. This should cause a graceful shutdown of the connection.
1 parent 1758569 commit 34a804d

9 files changed

Lines changed: 154 additions & 19 deletions

proxygen/lib/http/codec/ControlMessageRateLimitFilter.h

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,37 +24,61 @@ constexpr uint32_t kDefaultMaxDirectErrorHandlingPerInterval = 100;
2424
constexpr uint32_t kMaxDirectErrorHandlingPerIntervalLowerBound = 50;
2525
constexpr std::chrono::milliseconds kDefaultDirectErrorHandlingDuration{100};
2626

27+
constexpr uint32_t kDefaultMaxHeadersPerInterval = 500;
28+
constexpr uint32_t kMaxHeadersPerIntervalLowerBound = 100;
29+
constexpr std::chrono::milliseconds kDefaultHeadersDuration{100};
30+
31+
enum RateLimitTarget {
32+
CONTROL_MSGS,
33+
DIRECT_ERROR_HANDLING,
34+
HEADERS,
35+
};
36+
2737
/**
2838
* This class implements the rate limiting logic for control messages and
2939
* stream errors (that might produce HTTP error pages). If a rate limit is
3040
* exeeded, the callback is converted to a session level error with
3141
* ProxygenError = kErrorDropped. This is a signal to the codec callback that
3242
* the codec would like the connection dropped.
43+
*
44+
* TODO: Refactor this into separate filters, or group related parameters
45+
* into structs.
3346
*/
3447
class ControlMessageRateLimitFilter : public PassThroughHTTPCodecFilter {
3548
public:
3649
explicit ControlMessageRateLimitFilter(folly::HHWheelTimer* timer,
3750
HTTPSessionStats* httpSessionStats)
3851
: resetControlMessages_(numControlMsgsInCurrentInterval_,
52+
RateLimitTarget::CONTROL_MSGS,
3953
httpSessionStats),
54+
resetDirectErrors_(numDirectErrorHandlingInCurrentInterval_,
55+
RateLimitTarget::DIRECT_ERROR_HANDLING,
56+
httpSessionStats),
57+
resetHeaders_(numHeadersInCurrentInterval_,
58+
RateLimitTarget::HEADERS,
59+
httpSessionStats),
4060
timer_(timer),
4161
httpSessionStats_(httpSessionStats) {
4262
}
4363

4464
void setSessionStats(HTTPSessionStats* httpSessionStats) {
4565
httpSessionStats_ = httpSessionStats;
4666
resetControlMessages_.httpSessionStats = httpSessionStats;
67+
resetHeaders_.httpSessionStats = httpSessionStats;
4768
}
4869

49-
void setParams(
50-
uint32_t maxControlMsgsPerInterval,
51-
uint32_t maxDirectErrorHandlingPerInterval,
52-
std::chrono::milliseconds controlMsgIntervalDuration,
53-
std::chrono::milliseconds directErrorHandlingIntervalDuration) {
70+
void setParams(uint32_t maxControlMsgsPerInterval,
71+
uint32_t maxDirectErrorHandlingPerInterval,
72+
uint32_t maxHeadersPerInterval,
73+
std::chrono::milliseconds controlMsgIntervalDuration,
74+
std::chrono::milliseconds directErrorHandlingIntervalDuration,
75+
std::chrono::milliseconds headersIntervalDuration) {
5476
maxControlMsgsPerInterval_ = maxControlMsgsPerInterval;
5577
maxDirectErrorHandlingPerInterval_ = maxDirectErrorHandlingPerInterval;
5678
controlMsgIntervalDuration_ = controlMsgIntervalDuration;
5779
directErrorHandlingIntervalDuration_ = directErrorHandlingIntervalDuration;
80+
maxHeadersPerInterval_ = maxHeadersPerInterval;
81+
headersIntervalDuration_ = headersIntervalDuration;
5882
}
5983

6084
// Filter functions
@@ -86,6 +110,13 @@ class ControlMessageRateLimitFilter : public PassThroughHTTPCodecFilter {
86110
}
87111
}
88112

113+
void onHeadersComplete(StreamID stream,
114+
std::unique_ptr<HTTPMessage> msg) override {
115+
if (!incrementNumHeadersInCurInterval()) {
116+
callback_->onHeadersComplete(stream, std::move(msg));
117+
}
118+
}
119+
89120
void onError(HTTPCodec::StreamID streamID,
90121
const HTTPException& error,
91122
bool newTxn) override {
@@ -104,6 +135,7 @@ class ControlMessageRateLimitFilter : public PassThroughHTTPCodecFilter {
104135
void detachThreadLocals() {
105136
resetControlMessages_.cancelTimeout();
106137
resetDirectErrors_.cancelTimeout();
138+
resetHeaders_.cancelTimeout();
107139
timer_ = nullptr;
108140
// Free pass when switching threads
109141
numControlMsgsInCurrentInterval_ = 0;
@@ -140,6 +172,25 @@ class ControlMessageRateLimitFilter : public PassThroughHTTPCodecFilter {
140172
return false;
141173
}
142174

175+
bool incrementNumHeadersInCurInterval() {
176+
if (numHeadersInCurrentInterval_ == 0) {
177+
// The first header (or first after a reset) schedules the next
178+
// reset timer
179+
CHECK(timer_);
180+
timer_->scheduleTimeout(&resetHeaders_, headersIntervalDuration_);
181+
}
182+
183+
if (++numHeadersInCurrentInterval_ > maxHeadersPerInterval_) {
184+
if (httpSessionStats_) {
185+
httpSessionStats_->recordHeadersRateLimited();
186+
}
187+
callback_->onGoaway(http2::kMaxStreamID, ErrorCode::NO_ERROR);
188+
return true;
189+
}
190+
191+
return false;
192+
}
193+
143194
bool incrementDirectErrorHandlingInCurInterval() {
144195
if (numDirectErrorHandlingInCurrentInterval_ == 0) {
145196
// The first control message (or first after a reset) schedules the next
@@ -168,20 +219,34 @@ class ControlMessageRateLimitFilter : public PassThroughHTTPCodecFilter {
168219
class ResetCounterTimeout : public folly::HHWheelTimer::Callback {
169220
public:
170221
explicit ResetCounterTimeout(uint32_t& counterIn,
222+
RateLimitTarget rateLimitTargetIn,
171223
HTTPSessionStats* httpSessionStatsIn = nullptr)
172-
: counter(counterIn), httpSessionStats(httpSessionStatsIn) {
224+
: counter(counterIn),
225+
rateLimitTarget(rateLimitTargetIn),
226+
httpSessionStats(httpSessionStatsIn) {
173227
}
174228

175229
void timeoutExpired() noexcept override {
176230
if (counter > 0 && httpSessionStats) {
177-
httpSessionStats->recordControlMsgsInInterval(counter);
231+
switch (rateLimitTarget) {
232+
case RateLimitTarget::CONTROL_MSGS:
233+
httpSessionStats->recordControlMsgsInInterval(counter);
234+
break;
235+
case RateLimitTarget::DIRECT_ERROR_HANDLING:
236+
// No stats for this one
237+
break;
238+
case RateLimitTarget::HEADERS:
239+
httpSessionStats->recordHeadersInInterval(counter);
240+
break;
241+
}
178242
}
179243
counter = 0;
180244
}
181245
void callbackCanceled() noexcept override {
182246
}
183247

184248
uint32_t& counter;
249+
RateLimitTarget rateLimitTarget;
185250
HTTPSessionStats* httpSessionStats{nullptr};
186251
};
187252

@@ -197,14 +262,18 @@ class ControlMessageRateLimitFilter : public PassThroughHTTPCodecFilter {
197262
uint32_t maxDirectErrorHandlingPerInterval_{
198263
kDefaultMaxDirectErrorHandlingPerInterval};
199264

265+
uint32_t numHeadersInCurrentInterval_{0};
266+
uint32_t maxHeadersPerInterval_{kDefaultMaxHeadersPerInterval};
267+
200268
std::chrono::milliseconds controlMsgIntervalDuration_{
201269
kDefaultControlMsgDuration};
202270
std::chrono::milliseconds directErrorHandlingIntervalDuration_{
203271
kDefaultDirectErrorHandlingDuration};
272+
std::chrono::milliseconds headersIntervalDuration_{kDefaultHeadersDuration};
204273

205274
ResetCounterTimeout resetControlMessages_;
206-
ResetCounterTimeout resetDirectErrors_{
207-
numDirectErrorHandlingInCurrentInterval_};
275+
ResetCounterTimeout resetDirectErrors_;
276+
ResetCounterTimeout resetHeaders_;
208277
folly::HHWheelTimer* timer_{nullptr};
209278
HTTPSessionStats* httpSessionStats_{nullptr};
210279
};

proxygen/lib/http/session/HTTPSession.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,8 @@ void HTTPSession::setupCodec() {
222222
// existing flow control filter
223223
}
224224
if (codec_->supportsParallelRequests() && !controlMessageRateLimitFilter_ &&
225-
sock_) {
225+
sock_ &&
226+
codec_->getTransportDirection() == TransportDirection::DOWNSTREAM) {
226227
controlMessageRateLimitFilter_ = new ControlMessageRateLimitFilter(
227228
&getEventBase()->timer(), sessionStats_);
228229
codec_.addFilters(std::unique_ptr<ControlMessageRateLimitFilter>(

proxygen/lib/http/session/HTTPSessionBase.cpp

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,11 @@ void HTTPSessionBase::setSessionStats(HTTPSessionStats* stats) {
7575
void HTTPSessionBase::setControlMessageRateLimitParams(
7676
uint32_t maxControlMsgsPerInterval,
7777
uint32_t maxDirectErrorHandlingPerInterval,
78+
uint32_t maxHeadersPerInterval,
7879
std::chrono::milliseconds controlMsgIntervalDuration,
79-
std::chrono::milliseconds directErrorHandlingIntervalDuration) {
80+
std::chrono::milliseconds directErrorHandlingIntervalDuration,
81+
std::chrono::milliseconds headersIntervalDuration) {
82+
8083
if (maxControlMsgsPerInterval < kMaxControlMsgsPerIntervalLowerBound) {
8184
XLOG_EVERY_MS(WARNING, 60000)
8285
<< "Invalid maxControlMsgsPerInterval: " << maxControlMsgsPerInterval;
@@ -92,12 +95,20 @@ void HTTPSessionBase::setControlMessageRateLimitParams(
9295
kMaxDirectErrorHandlingPerIntervalLowerBound;
9396
}
9497

98+
if (maxHeadersPerInterval < kMaxHeadersPerIntervalLowerBound) {
99+
XLOG_EVERY_MS(WARNING, 60000)
100+
<< "Invalid maxHeadersPerInterval: " << maxHeadersPerInterval;
101+
maxHeadersPerInterval = kMaxHeadersPerIntervalLowerBound;
102+
}
103+
95104
if (controlMessageRateLimitFilter_) {
96105
controlMessageRateLimitFilter_->setParams(
97106
maxControlMsgsPerInterval,
98107
maxDirectErrorHandlingPerInterval,
108+
maxHeadersPerInterval,
99109
controlMsgIntervalDuration,
100-
directErrorHandlingIntervalDuration);
110+
directErrorHandlingIntervalDuration,
111+
headersIntervalDuration);
101112
}
102113
}
103114

proxygen/lib/http/session/HTTPSessionBase.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,13 @@ class HTTPSessionBase : public wangle::ManagedConnection {
176176
uint32_t maxControlMsgsPerInterval = kDefaultMaxControlMsgsPerInterval,
177177
uint32_t maxDirectErrorHandlingPerInterval =
178178
kDefaultMaxDirectErrorHandlingPerInterval,
179+
uint32_t maxHeadersPerInterval = kDefaultMaxHeadersPerInterval,
179180
std::chrono::milliseconds controlMsgIntervalDuration =
180181
kDefaultControlMsgDuration,
181182
std::chrono::milliseconds directErrorHandlingIntervalDuration =
182-
kDefaultDirectErrorHandlingDuration);
183+
kDefaultDirectErrorHandlingDuration,
184+
std::chrono::milliseconds headersIntervalDuration =
185+
kDefaultHeadersDuration);
183186

184187
InfoCallback* getInfoCallback() const {
185188
return infoCallback_;

proxygen/lib/http/session/HTTPSessionStats.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ class HTTPSessionStats : public TTLBAStats {
3838
}
3939
virtual void recordControlMsgRateLimited() noexcept {
4040
}
41+
virtual void recordHeadersInInterval(int64_t) noexcept {
42+
}
43+
virtual void recordHeadersRateLimited() noexcept {
44+
}
4145
};
4246

4347
} // namespace proxygen

proxygen/lib/http/session/test/HTTPDownstreamSessionTest.cpp

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ class HTTPDownstreamTest : public testing::Test {
9696

9797
httpSession_->setFlowControl(
9898
flowControl[0], flowControl[1], flowControl[2]);
99-
httpSession_->setEgressSettings({{SettingsId::MAX_CONCURRENT_STREAMS, 80},
99+
httpSession_->setEgressSettings({{SettingsId::MAX_CONCURRENT_STREAMS, 200},
100100
{SettingsId::HEADER_TABLE_SIZE, 5555},
101101
{SettingsId::ENABLE_PUSH, 1},
102102
{SettingsId::ENABLE_EX_HEADERS, 1}});
@@ -3727,6 +3727,30 @@ TEST_F(HTTP2DownstreamSessionTest, TestPriorityFCBlocked) {
37273727
this->eventBase_.loop();
37283728
}
37293729

3730+
TEST_F(HTTP2DownstreamSessionTest, TestHeadersRateLimitExceeded) {
3731+
httpSession_->setControlMessageRateLimitParams(
3732+
1000, 1000, 100, seconds(0), seconds(0), seconds(0));
3733+
3734+
std::vector<std::unique_ptr<testing::StrictMock<MockHTTPHandler>>> handlers;
3735+
for (int i = 0; i < 100; i++) {
3736+
auto handler = addSimpleStrictHandler();
3737+
auto rawHandler = handler.get();
3738+
handlers.push_back(std::move(handler));
3739+
rawHandler->expectHeaders();
3740+
rawHandler->expectEOM(
3741+
[rawHandler] { rawHandler->sendReplyWithBody(200, 100); });
3742+
sendRequest();
3743+
}
3744+
// Straw that breaks the camel's back
3745+
sendRequest();
3746+
for (int i = 0; i < 100; i++) {
3747+
handlers[i]->expectGoaway();
3748+
handlers[i]->expectDetachTransaction();
3749+
}
3750+
expectDetachSession();
3751+
flushRequestsAndLoopN(2);
3752+
}
3753+
37303754
TEST_F(HTTP2DownstreamSessionTest, TestControlMsgRateLimitExceeded) {
37313755
auto streamid = clientCodec_->createStream();
37323756

@@ -3756,7 +3780,7 @@ TEST_F(HTTP2DownstreamSessionTest, TestControlMsgResetRateLimitTouched) {
37563780

37573781
auto streamid = clientCodec_->createStream();
37583782

3759-
httpSession_->setControlMessageRateLimitParams(100, 100, milliseconds(0));
3783+
httpSession_->setControlMessageRateLimitParams(10, 100, 100, milliseconds(0));
37603784

37613785
// Send 97 PRIORITY, 1 SETTINGS, and 2 PING frames. This doesn't exceed the
37623786
// limit of 10.
@@ -3797,7 +3821,7 @@ TEST_F(HTTP2DownstreamSessionTest, TestControlMsgResetRateLimitTouched) {
37973821
}
37983822

37993823
TEST_F(HTTP2DownstreamSessionTest, DirectErrorHandlingLimitTouched) {
3800-
httpSession_->setControlMessageRateLimitParams(100, 50, milliseconds(0));
3824+
httpSession_->setControlMessageRateLimitParams(100, 10, 100, milliseconds(0));
38013825

38023826
// Send 50 messages, each of which cause direct error handling. Since
38033827
// this doesn't exceed the limit, this should not cause the connection
@@ -3829,7 +3853,7 @@ TEST_F(HTTP2DownstreamSessionTest, DirectErrorHandlingLimitTouched) {
38293853
}
38303854

38313855
TEST_F(HTTP2DownstreamSessionTest, DirectErrorHandlingLimitExceeded) {
3832-
httpSession_->setControlMessageRateLimitParams(100, 50, milliseconds(0));
3856+
httpSession_->setControlMessageRateLimitParams(100, 10, 100, milliseconds(0));
38333857

38343858
// Send eleven messages, each of which causes direct error handling. Since
38353859
// this exceeds the limit, the connection should be dropped.

proxygen/lib/http/session/test/HTTPUpstreamSessionTest.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2931,7 +2931,7 @@ TEST_F(HTTP2UpstreamSessionTest, AttachDetach) {
29312931
httpSession_->detachThreadLocals();
29322932
httpSession_->attachThreadLocals(
29332933
&base, nullptr, timerInstance, nullptr, fn, nullptr, nullptr);
2934-
EXPECT_EQ(filterCount, 3);
2934+
EXPECT_EQ(filterCount, 2);
29352935
filterCount = 0;
29362936
base.loopOnce();
29372937
}
@@ -2997,7 +2997,7 @@ TEST_F(HTTP2UpstreamSessionTest, DetachFlowControlTimeout) {
29972997
httpSession_->detachThreadLocals();
29982998
httpSession_->attachThreadLocals(
29992999
&base, nullptr, timerInstance, nullptr, fn, nullptr, nullptr);
3000-
EXPECT_EQ(filterCount, 3);
3000+
EXPECT_EQ(filterCount, 2);
30013001
filterCount = 0;
30023002
base.loopOnce();
30033003
}

proxygen/lib/http/stats/ThreadLocalHTTPSessionStats.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ TLHTTPSessionStats::TLHTTPSessionStats(const std::string& prefix)
3939
ttbtxExceedLimit(prefix + "_ttbtx_exceed_limit", facebook::fb303::SUM),
4040
ctrlMsgsRateLimited(prefix + "_ctrl_msgs_rate_limited",
4141
facebook::fb303::SUM),
42+
headersRateLimited(prefix + "_headers_rate_limited",
43+
facebook::fb303::SUM),
4244
txnsPerSession(prefix + "_txn_per_session",
4345
1,
4446
0,
@@ -64,6 +66,15 @@ TLHTTPSessionStats::TLHTTPSessionStats(const std::string& prefix)
6466
facebook::fb303::AVG,
6567
50,
6668
99,
69+
100),
70+
headersInInterval(
71+
prefix + "_headers_in_interval",
72+
1 /* bucketWidth */,
73+
0 /* min */,
74+
500 /* max, keep in sync with kDefaultMaxHeadersMsgsPerInterval */,
75+
facebook::fb303::AVG,
76+
50,
77+
99,
6778
100) {
6879
}
6980

@@ -172,4 +183,12 @@ void TLHTTPSessionStats::recordControlMsgRateLimited() noexcept {
172183
ctrlMsgsRateLimited.add(1);
173184
}
174185

186+
void TLHTTPSessionStats::recordHeadersInInterval(int64_t quantity) noexcept {
187+
headersInInterval.add(quantity);
188+
}
189+
190+
void TLHTTPSessionStats::recordHeadersRateLimited() noexcept {
191+
headersRateLimited.add(1);
192+
}
193+
175194
} // namespace proxygen

proxygen/lib/http/stats/ThreadLocalHTTPSessionStats.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ class TLHTTPSessionStats : public HTTPSessionStats {
4444

4545
void recordControlMsgsInInterval(int64_t quantity) noexcept override;
4646
void recordControlMsgRateLimited() noexcept override;
47+
void recordHeadersInInterval(int64_t quantity) noexcept override;
48+
void recordHeadersRateLimited() noexcept override;
4749

4850
BaseStats::TLCounter txnsOpen;
4951
BaseStats::TLCounter pendingBufferedReadBytes;
@@ -68,9 +70,11 @@ class TLHTTPSessionStats : public HTTPSessionStats {
6870
BaseStats::TLTimeseries ttbtxNotFound;
6971
BaseStats::TLTimeseries ttbtxExceedLimit;
7072
BaseStats::TLTimeseries ctrlMsgsRateLimited;
73+
BaseStats::TLTimeseries headersRateLimited;
7174
BaseStats::TLHistogram txnsPerSession;
7275
BaseStats::TLHistogram sessionIdleTime;
7376
BaseStats::TLHistogram ctrlMsgsInInterval;
77+
BaseStats::TLHistogram headersInInterval;
7478
};
7579

7680
} // namespace proxygen

0 commit comments

Comments
 (0)