Skip to content

Commit 7184d1e

Browse files
authored
[EXPORTER] OTLP HTTP exporter reads unbounded response (open-telemetry#4078)
1 parent f0c7212 commit 7184d1e

3 files changed

Lines changed: 123 additions & 7 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,21 @@ Increment the:
107107
* [BUILD] Upgrade to rapidyaml 0.12.1
108108
[#4076](https://github.com/open-telemetry/opentelemetry-cpp/pull/4076)
109109

110+
* [EXPORTER] OTLP HTTP exporters read unbounded HTTP response
111+
[#4078](https://github.com/open-telemetry/opentelemetry-cpp/pull/4078)
112+
113+
Security fix:
114+
115+
* [EXPORTER] OTLP HTTP exporters read unbounded HTTP response
116+
[#4078](https://github.com/open-telemetry/opentelemetry-cpp/pull/4078)
117+
118+
* When exporting OTLP HTTP data to a misconfigured or malicious endpoint,
119+
the exporter could allocate an arbitrary amount of memory when getting
120+
the endpoint HTTP response back.
121+
* The size of HTTP responses is now limited to 4MiB by default,
122+
following the opentelemetry-proto recommendations.
123+
* See CVE-2026-44967
124+
110125
Important changes:
111126

112127
* Enable WITH_OTLP_RETRY_PREVIEW by default

‎ext/include/opentelemetry/ext/http/client/curl/http_operation_curl.h‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ const std::chrono::milliseconds kDefaultHttpConnTimeout(5000); // ms
4343
const std::string kHttpStatusRegexp = "HTTP\\/\\d\\.\\d (\\d+)\\ .*";
4444
const std::string kHttpHeaderRegexp = "(.*)\\: (.*)\\n*";
4545

46+
/**
47+
* Default max HTTP Response size.
48+
* 4MiB
49+
* @see
50+
* https://github.com/open-telemetry/opentelemetry-proto/blob/main/docs/specification.md#otlphttp-response
51+
*/
52+
const size_t kDefaultMaxResponseSize = 4 * 1024 * 1024;
53+
4654
class HttpClient;
4755
class Session;
4856

@@ -198,6 +206,12 @@ class HttpOperation
198206
*/
199207
std::chrono::system_clock::time_point NextRetryTime();
200208

209+
/**
210+
* Limit memory consumption on HTTP response.
211+
* Size is @c kDefaultMaxResponseSize by default.
212+
*/
213+
void SetMaxResponseSize(size_t max_size) { max_response_size_ = max_size; }
214+
201215
/**
202216
* Setup request
203217
*/
@@ -342,6 +356,8 @@ class HttpOperation
342356
std::vector<uint8_t> response_headers_;
343357
std::vector<uint8_t> response_body_;
344358
std::vector<uint8_t> raw_response_;
359+
/** Max HTTP response size. */
360+
size_t max_response_size_{kDefaultMaxResponseSize};
345361

346362
struct AsyncData
347363
{

‎ext/src/http/client/curl/http_operation_curl.cc‎

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,43 @@ size_t HttpOperation::WriteMemoryCallback(void *contents, size_t size, size_t nm
7777
return 0;
7878
}
7979

80-
self->raw_response_.insert(self->raw_response_.end(), static_cast<char *>(contents),
81-
static_cast<char *>(contents) + (size * nmemb));
80+
const size_t data_size = size * nmemb;
81+
82+
// This code is defensive on purpose, to avoid allocation of unbound
83+
// amounts of memory, controlled by the (remote) endpoint response.
84+
85+
// 1: Check data_size did not overflow
86+
if (nmemb != 0)
87+
{
88+
if (data_size / nmemb != size)
89+
{
90+
// Should be impossible, really:
91+
// CURL will report a huge response small chunks at a time,
92+
// not in one block, which would be a DOS in CURL already.
93+
return 0;
94+
}
95+
}
96+
97+
// 2: Check internal integrity
98+
if (self->raw_response_.size() > self->max_response_size_)
99+
{
100+
// Should be impossible,
101+
// this means the previous call did exceed the max size already.
102+
return 0;
103+
}
104+
105+
// 3: Protect against memory exhaustion caused by the remote endpoint
106+
if (data_size > self->max_response_size_ - self->raw_response_.size())
107+
{
108+
// This one is possible and must be protected against (CVE-2026-44967).
109+
// Checks 1 and 2 ensure the math is correct.
110+
return 0;
111+
}
112+
113+
const unsigned char *begin = static_cast<unsigned char *>(contents);
114+
const unsigned char *end = begin + data_size;
115+
116+
self->raw_response_.insert(self->raw_response_.end(), begin, end);
82117

83118
if (self->WasAborted())
84119
{
@@ -95,7 +130,7 @@ size_t HttpOperation::WriteMemoryCallback(void *contents, size_t size, size_t nm
95130
self->DispatchEvent(opentelemetry::ext::http::client::SessionState::Sending);
96131
}
97132

98-
return size * nmemb;
133+
return data_size;
99134
}
100135

101136
size_t HttpOperation::WriteVectorHeaderCallback(void *ptr, size_t size, size_t nmemb, void *userp)
@@ -106,8 +141,33 @@ size_t HttpOperation::WriteVectorHeaderCallback(void *ptr, size_t size, size_t n
106141
return 0;
107142
}
108143

144+
const size_t data_size = size * nmemb;
145+
146+
// See comments in HttpOperation::WriteMemoryCallback().
147+
148+
if (nmemb != 0)
149+
{
150+
if (data_size / nmemb != size)
151+
{
152+
return 0;
153+
}
154+
}
155+
156+
// Common limit for header + body
157+
if (self->response_headers_.size() + self->response_body_.size() > self->max_response_size_)
158+
{
159+
return 0;
160+
}
161+
162+
if (data_size >
163+
self->max_response_size_ - self->response_headers_.size() - self->response_body_.size())
164+
{
165+
return 0;
166+
}
167+
109168
const unsigned char *begin = static_cast<unsigned char *>(ptr);
110-
const unsigned char *end = begin + size * nmemb;
169+
const unsigned char *end = begin + data_size;
170+
111171
self->response_headers_.insert(self->response_headers_.end(), begin, end);
112172

113173
if (self->WasAborted())
@@ -125,7 +185,7 @@ size_t HttpOperation::WriteVectorHeaderCallback(void *ptr, size_t size, size_t n
125185
self->DispatchEvent(opentelemetry::ext::http::client::SessionState::Sending);
126186
}
127187

128-
return size * nmemb;
188+
return data_size;
129189
}
130190

131191
size_t HttpOperation::WriteVectorBodyCallback(void *ptr, size_t size, size_t nmemb, void *userp)
@@ -136,8 +196,33 @@ size_t HttpOperation::WriteVectorBodyCallback(void *ptr, size_t size, size_t nme
136196
return 0;
137197
}
138198

199+
const size_t data_size = size * nmemb;
200+
201+
// See comments in HttpOperation::WriteMemoryCallback().
202+
203+
if (nmemb != 0)
204+
{
205+
if (data_size / nmemb != size)
206+
{
207+
return 0;
208+
}
209+
}
210+
211+
// Common limit for header + body
212+
if (self->response_headers_.size() + self->response_body_.size() > self->max_response_size_)
213+
{
214+
return 0;
215+
}
216+
217+
if (data_size >
218+
self->max_response_size_ - self->response_headers_.size() - self->response_body_.size())
219+
{
220+
return 0;
221+
}
222+
139223
const unsigned char *begin = static_cast<unsigned char *>(ptr);
140-
const unsigned char *end = begin + size * nmemb;
224+
const unsigned char *end = begin + data_size;
225+
141226
self->response_body_.insert(self->response_body_.end(), begin, end);
142227

143228
if (self->WasAborted())
@@ -155,7 +240,7 @@ size_t HttpOperation::WriteVectorBodyCallback(void *ptr, size_t size, size_t nme
155240
self->DispatchEvent(opentelemetry::ext::http::client::SessionState::Sending);
156241
}
157242

158-
return size * nmemb;
243+
return data_size;
159244
}
160245

161246
size_t HttpOperation::ReadMemoryCallback(char *buffer, size_t size, size_t nitems, void *userp)

0 commit comments

Comments
 (0)