CVE-2026-44967
Description
OpenTelemetry-cpp OTLP HTTP exporters before 1.27.0 read unbounded HTTP response bodies, enabling memory exhaustion when the collector is attacker-controlled or MITM.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
OpenTelemetry-cpp OTLP HTTP exporters before 1.27.0 read unbounded HTTP response bodies, enabling memory exhaustion when the collector is attacker-controlled or MITM.
Vulnerability
The OTLP HTTP exporters for traces, metrics, and logs in OpenTelemetry-cpp prior to version 1.27.0 read the full HTTP response body into an in-memory std::vector<uint8_t> without enforcing a size cap [1][3][4]. The affected code resides in exporters/otlp/src/otlp_http_client.cc and related HTTP client headers [4]. The vulnerability is reachable by any application using the opentelemetry-cpp SDK with an OTLP HTTP exporter configured [3].
Exploitation
An attacker must control the collector endpoint to which the exporter sends telemetry data, or be able to perform a man-in-the-middle (MITM) attack on the network connection between the exporter and the collector [1][3]. By returning an arbitrarily large HTTP response body, the attacker causes the exporter to read that payload in full into memory using calls such as io.Copy (in the Go analogy) or the equivalent C++ std::vector resize/copy operations [1][4]. No authentication or special privileges are required beyond network position.
Impact
Successful exploitation leads to memory exhaustion, which can crash the process (denial of service) [1][3]. The attacker does not gain code execution or data access; the impact is limited to availability via resource exhaustion. The vulnerability is rated Medium (CVSS 5.3) as it requires the collector endpoint to be untrusted or the network segment to be compromisable.
Mitigation
The vulnerability is fixed in OpenTelemetry-cpp release 1.27.0, which introduces a maximum size limit on HTTP response body parsing [2][3]. Users should upgrade to version 1.27.0 and link with the fixed library [3]. There is no known workaround other than ensuring the collector endpoint is trusted and the network path is secure. No KEV listing is available at this time.
AI Insight generated on Jun 12, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2(expand)+ 1 more
- (no CPE)
- (no CPE)range: <1.27.0
Patches
27184d1edde9d[EXPORTER] OTLP HTTP exporter reads unbounded response (#4078)
3 files changed · +123 −7
CHANGELOG.md+15 −0 modified@@ -107,6 +107,21 @@ Increment the: * [BUILD] Upgrade to rapidyaml 0.12.1 [#4076](https://github.com/open-telemetry/opentelemetry-cpp/pull/4076) +* [EXPORTER] OTLP HTTP exporters read unbounded HTTP response + [#4078](https://github.com/open-telemetry/opentelemetry-cpp/pull/4078) + +Security fix: + +* [EXPORTER] OTLP HTTP exporters read unbounded HTTP response + [#4078](https://github.com/open-telemetry/opentelemetry-cpp/pull/4078) + + * When exporting OTLP HTTP data to a misconfigured or malicious endpoint, + the exporter could allocate an arbitrary amount of memory when getting + the endpoint HTTP response back. + * The size of HTTP responses is now limited to 4MiB by default, + following the opentelemetry-proto recommendations. + * See CVE-2026-44967 + Important changes: * Enable WITH_OTLP_RETRY_PREVIEW by default
ext/include/opentelemetry/ext/http/client/curl/http_operation_curl.h+16 −0 modified@@ -43,6 +43,14 @@ const std::chrono::milliseconds kDefaultHttpConnTimeout(5000); // ms const std::string kHttpStatusRegexp = "HTTP\\/\\d\\.\\d (\\d+)\\ .*"; const std::string kHttpHeaderRegexp = "(.*)\\: (.*)\\n*"; +/** + * Default max HTTP Response size. + * 4MiB + * @see + * https://github.com/open-telemetry/opentelemetry-proto/blob/main/docs/specification.md#otlphttp-response + */ +const size_t kDefaultMaxResponseSize = 4 * 1024 * 1024; + class HttpClient; class Session; @@ -198,6 +206,12 @@ class HttpOperation */ std::chrono::system_clock::time_point NextRetryTime(); + /** + * Limit memory consumption on HTTP response. + * Size is @c kDefaultMaxResponseSize by default. + */ + void SetMaxResponseSize(size_t max_size) { max_response_size_ = max_size; } + /** * Setup request */ @@ -342,6 +356,8 @@ class HttpOperation std::vector<uint8_t> response_headers_; std::vector<uint8_t> response_body_; std::vector<uint8_t> raw_response_; + /** Max HTTP response size. */ + size_t max_response_size_{kDefaultMaxResponseSize}; struct AsyncData {
ext/src/http/client/curl/http_operation_curl.cc+92 −7 modified@@ -77,8 +77,43 @@ size_t HttpOperation::WriteMemoryCallback(void *contents, size_t size, size_t nm return 0; } - self->raw_response_.insert(self->raw_response_.end(), static_cast<char *>(contents), - static_cast<char *>(contents) + (size * nmemb)); + const size_t data_size = size * nmemb; + + // This code is defensive on purpose, to avoid allocation of unbound + // amounts of memory, controlled by the (remote) endpoint response. + + // 1: Check data_size did not overflow + if (nmemb != 0) + { + if (data_size / nmemb != size) + { + // Should be impossible, really: + // CURL will report a huge response small chunks at a time, + // not in one block, which would be a DOS in CURL already. + return 0; + } + } + + // 2: Check internal integrity + if (self->raw_response_.size() > self->max_response_size_) + { + // Should be impossible, + // this means the previous call did exceed the max size already. + return 0; + } + + // 3: Protect against memory exhaustion caused by the remote endpoint + if (data_size > self->max_response_size_ - self->raw_response_.size()) + { + // This one is possible and must be protected against (CVE-2026-44967). + // Checks 1 and 2 ensure the math is correct. + return 0; + } + + const unsigned char *begin = static_cast<unsigned char *>(contents); + const unsigned char *end = begin + data_size; + + self->raw_response_.insert(self->raw_response_.end(), begin, end); if (self->WasAborted()) { @@ -95,7 +130,7 @@ size_t HttpOperation::WriteMemoryCallback(void *contents, size_t size, size_t nm self->DispatchEvent(opentelemetry::ext::http::client::SessionState::Sending); } - return size * nmemb; + return data_size; } 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 return 0; } + const size_t data_size = size * nmemb; + + // See comments in HttpOperation::WriteMemoryCallback(). + + if (nmemb != 0) + { + if (data_size / nmemb != size) + { + return 0; + } + } + + // Common limit for header + body + if (self->response_headers_.size() + self->response_body_.size() > self->max_response_size_) + { + return 0; + } + + if (data_size > + self->max_response_size_ - self->response_headers_.size() - self->response_body_.size()) + { + return 0; + } + const unsigned char *begin = static_cast<unsigned char *>(ptr); - const unsigned char *end = begin + size * nmemb; + const unsigned char *end = begin + data_size; + self->response_headers_.insert(self->response_headers_.end(), begin, end); if (self->WasAborted()) @@ -125,7 +185,7 @@ size_t HttpOperation::WriteVectorHeaderCallback(void *ptr, size_t size, size_t n self->DispatchEvent(opentelemetry::ext::http::client::SessionState::Sending); } - return size * nmemb; + return data_size; } 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 return 0; } + const size_t data_size = size * nmemb; + + // See comments in HttpOperation::WriteMemoryCallback(). + + if (nmemb != 0) + { + if (data_size / nmemb != size) + { + return 0; + } + } + + // Common limit for header + body + if (self->response_headers_.size() + self->response_body_.size() > self->max_response_size_) + { + return 0; + } + + if (data_size > + self->max_response_size_ - self->response_headers_.size() - self->response_body_.size()) + { + return 0; + } + const unsigned char *begin = static_cast<unsigned char *>(ptr); - const unsigned char *end = begin + size * nmemb; + const unsigned char *end = begin + data_size; + self->response_body_.insert(self->response_body_.end(), begin, end); if (self->WasAborted()) @@ -155,7 +240,7 @@ size_t HttpOperation::WriteVectorBodyCallback(void *ptr, size_t size, size_t nme self->DispatchEvent(opentelemetry::ext::http::client::SessionState::Sending); } - return size * nmemb; + return data_size; } size_t HttpOperation::ReadMemoryCallback(char *buffer, size_t size, size_t nitems, void *userp)
5e363de517dblimit response body size for OTLP HTTP exporters (#8108)
8 files changed · +217 −6
CHANGELOG.md+1 −0 modified@@ -35,6 +35,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Return spec-compliant `TraceIdRatioBased` description. This is a breaking behavioral change, but it is necessary to make the implementation [spec-compliant](https://opentelemetry.io/docs/specs/otel/trace/sdk/#traceidratiobased). (#8027) - Fix a race condition in `go.opentelemetry.io/otel/sdk/metric` where the lastvalue aggregation could collect the value 0 even when no zero-value measurements were recorded. (#8056) +- Limit HTTP response body to 4 MiB in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp`, `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`, and `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp` to mitigate excessive memory usage caused by a misconfigured or malicious server. Responses exceeding the limit are treated as non-retryable errors. (#8108) - `WithHostID` detector in `go.opentelemetry.io/otel/sdk/resource` to use full path for `kenv` command on BSD. (#8113) - Fix missing `request.GetBody` in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp` to correctly handle HTTP2 GOAWAY frame. (#8096)
exporters/otlp/otlplog/otlploghttp/client.go+17 −2 modified@@ -47,6 +47,13 @@ var exporterN atomic.Int64 var errInsecureEndpointWithTLS = errors.New("insecure HTTP endpoint cannot use TLS client configuration") +// maxResponseBodySize is the maximum number of bytes to read from a response +// body. It is set to 4 MiB per the OTLP specification recommendation to +// mitigate excessive memory usage caused by a misconfigured or malicious +// server. If exceeded, the response is treated as a not-retryable error. +// This is a variable to allow tests to override it. +var maxResponseBodySize int64 = 4 * 1024 * 1024 + // nextExporterID returns the next unique ID for an exporter. func nextExporterID() int64 { const inc = 1 @@ -194,7 +201,11 @@ func (c *httpClient) uploadLogs(ctx context.Context, data []*logpb.ResourceLogs) // Read the partial success message, if any. var respData bytes.Buffer - if _, err := io.Copy(&respData, resp.Body); err != nil { + if _, err := io.Copy(&respData, http.MaxBytesReader(nil, resp.Body, maxResponseBodySize)); err != nil { + var maxBytesErr *http.MaxBytesError + if errors.As(err, &maxBytesErr) { + return fmt.Errorf("response body too large: exceeded %d bytes", maxBytesErr.Limit) + } return err } if respData.Len() == 0 { @@ -225,7 +236,11 @@ func (c *httpClient) uploadLogs(ctx context.Context, data []*logpb.ResourceLogs) // message to be returned. It will help in // debugging the actual issue. var respData bytes.Buffer - if _, err := io.Copy(&respData, resp.Body); err != nil { + if _, err := io.Copy(&respData, http.MaxBytesReader(nil, resp.Body, maxResponseBodySize)); err != nil { + var maxBytesErr *http.MaxBytesError + if errors.As(err, &maxBytesErr) { + return fmt.Errorf("response body too large: exceeded %d bytes", maxBytesErr.Limit) + } return err } respStr := strings.TrimSpace(respData.String())
exporters/otlp/otlplog/otlploghttp/client_test.go+52 −0 modified@@ -1017,6 +1017,58 @@ func TestClientInstrumentation(t *testing.T) { metricdatatest.AssertEqual(t, want, got.ScopeMetrics[0], opt...) } +func TestResponseBodySizeLimit(t *testing.T) { + // Override the limit to 1 byte so any non-empty response body exceeds it. + orig := maxResponseBodySize + maxResponseBodySize = 1 + t.Cleanup(func() { maxResponseBodySize = orig }) + + // largeBody is larger than the 1-byte limit. + largeBody := []byte("xx") + + tests := []struct { + name string + status int + contentType string + }{ + { + name: "success response body too large", + status: http.StatusOK, + contentType: "application/x-protobuf", + }, + { + name: "error response body too large", + status: http.StatusServiceUnavailable, + contentType: "text/plain", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var calls int + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + calls++ + w.Header().Set("Content-Type", tc.contentType) + w.WriteHeader(tc.status) + _, _ = w.Write(largeBody) + })) + t.Cleanup(srv.Close) + + opts := []Option{ + WithEndpoint(srv.Listener.Addr().String()), + WithInsecure(), + WithRetry(RetryConfig{Enabled: false}), + } + cfg := newConfig(opts) + c, err := newHTTPClient(t.Context(), cfg) + require.NoError(t, err) + + err = c.UploadLogs(t.Context(), make([]*lpb.ResourceLogs, 1)) + assert.ErrorContains(t, err, "response body too large") + assert.Equal(t, 1, calls, "request must not be retried after body-too-large error") + }) + } +} + func BenchmarkExporterExportLogs(b *testing.B) { const n = 10
exporters/otlp/otlpmetric/otlpmetrichttp/client.go+17 −2 modified@@ -54,6 +54,13 @@ var ourTransport = &http.Transport{ var errInsecureEndpointWithTLS = errors.New("insecure HTTP endpoint cannot use TLS client configuration") +// maxResponseBodySize is the maximum number of bytes to read from a response +// body. It is set to 4 MiB per the OTLP specification recommendation to +// mitigate excessive memory usage caused by a misconfigured or malicious +// server. If exceeded, the response is treated as a not-retryable error. +// This is a variable to allow tests to override it. +var maxResponseBodySize int64 = 4 * 1024 * 1024 + // newClient creates a new HTTP metric client. func newClient(cfg oconf.Config) (*client, error) { if cfg.Metrics.Insecure && cfg.Metrics.TLSCfg != nil { @@ -174,7 +181,11 @@ func (c *client) UploadMetrics(ctx context.Context, protoMetrics *metricpb.Resou // Read the partial success message, if any. var respData bytes.Buffer - if _, err := io.Copy(&respData, resp.Body); err != nil { + if _, err := io.Copy(&respData, http.MaxBytesReader(nil, resp.Body, maxResponseBodySize)); err != nil { + var maxBytesErr *http.MaxBytesError + if errors.As(err, &maxBytesErr) { + return fmt.Errorf("response body too large: exceeded %d bytes", maxBytesErr.Limit) + } return err } if respData.Len() == 0 { @@ -205,7 +216,11 @@ func (c *client) UploadMetrics(ctx context.Context, protoMetrics *metricpb.Resou // message to be returned. It will help in // debugging the actual issue. var respData bytes.Buffer - if _, err := io.Copy(&respData, resp.Body); err != nil { + if _, err := io.Copy(&respData, http.MaxBytesReader(nil, resp.Body, maxResponseBodySize)); err != nil { + var maxBytesErr *http.MaxBytesError + if errors.As(err, &maxBytesErr) { + return fmt.Errorf("response body too large: exceeded %d bytes", maxBytesErr.Limit) + } return err } respStr := strings.TrimSpace(respData.String())
exporters/otlp/otlpmetric/otlpmetrichttp/client_test.go+53 −0 modified@@ -379,3 +379,56 @@ func TestGetBodyCalledOnRedirect(t *testing.T) { assert.NotEmpty(t, requestBodies[0], "original request body should not be empty") assert.Equal(t, requestBodies[0], requestBodies[1], "redirect body should match original") } + +func TestResponseBodySizeLimit(t *testing.T) { + // Override the limit to 1 byte so any non-empty response body exceeds it. + orig := maxResponseBodySize + maxResponseBodySize = 1 + t.Cleanup(func() { maxResponseBodySize = orig }) + + // largeBody is larger than the 1-byte limit. + largeBody := []byte("xx") + + tests := []struct { + name string + status int + contentType string + }{ + { + name: "success response body too large", + status: http.StatusOK, + contentType: "application/x-protobuf", + }, + { + name: "error response body too large", + status: http.StatusServiceUnavailable, + contentType: "text/plain", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var calls int + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + calls++ + w.Header().Set("Content-Type", tc.contentType) + w.WriteHeader(tc.status) + _, _ = w.Write(largeBody) + })) + t.Cleanup(srv.Close) + + opts := []Option{ + WithEndpoint(srv.Listener.Addr().String()), + WithInsecure(), + WithRetry(RetryConfig{Enabled: false}), + } + cfg := oconf.NewHTTPConfig(asHTTPOptions(opts)...) + c, err := newClient(cfg) + require.NoError(t, err) + t.Cleanup(func() { _ = c.Shutdown(t.Context()) }) + + err = c.UploadMetrics(t.Context(), &mpb.ResourceMetrics{}) + assert.ErrorContains(t, err, "response body too large") + assert.Equal(t, 1, calls, "request must not be retried after body-too-large error") + }) + } +}
exporters/otlp/otlptrace/otlptracehttp/client.go+17 −2 modified@@ -32,6 +32,13 @@ import ( const contentTypeProto = "application/x-protobuf" +// maxResponseBodySize is the maximum number of bytes to read from a response +// body. It is set to 4 MiB per the OTLP specification recommendation to +// mitigate excessive memory usage caused by a misconfigured or malicious +// server. If exceeded, the response is treated as a not-retryable error. +// This is a variable to allow tests to override it. +var maxResponseBodySize int64 = 4 * 1024 * 1024 + var gzPool = sync.Pool{ New: func() any { w := gzip.NewWriter(io.Discard) @@ -203,7 +210,11 @@ func (d *client) UploadTraces(ctx context.Context, protoSpans []*tracepb.Resourc // Success, do not retry. // Read the partial success message, if any. var respData bytes.Buffer - if _, err := io.Copy(&respData, resp.Body); err != nil { + if _, err := io.Copy(&respData, http.MaxBytesReader(nil, resp.Body, maxResponseBodySize)); err != nil { + var maxBytesErr *http.MaxBytesError + if errors.As(err, &maxBytesErr) { + return fmt.Errorf("response body too large: exceeded %d bytes", maxBytesErr.Limit) + } return err } if respData.Len() == 0 { @@ -234,7 +245,11 @@ func (d *client) UploadTraces(ctx context.Context, protoSpans []*tracepb.Resourc // message to be returned. It will help in // debugging the actual issue. var respData bytes.Buffer - if _, err := io.Copy(&respData, resp.Body); err != nil { + if _, err := io.Copy(&respData, http.MaxBytesReader(nil, resp.Body, maxResponseBodySize)); err != nil { + var maxBytesErr *http.MaxBytesError + if errors.As(err, &maxBytesErr) { + return fmt.Errorf("response body too large: exceeded %d bytes", maxBytesErr.Limit) + } return err } respStr := strings.TrimSpace(respData.String())
exporters/otlp/otlptrace/otlptracehttp/client_test.go+52 −0 modified@@ -597,6 +597,58 @@ func TestClientInstrumentation(t *testing.T) { metricdatatest.AssertEqual(t, want, got.ScopeMetrics[0], opt...) } +func TestResponseBodySizeLimit(t *testing.T) { + // Override the limit to 1 byte so any non-empty response body exceeds it. + orig := *otlptracehttp.MaxResponseBodySize + *otlptracehttp.MaxResponseBodySize = 1 + t.Cleanup(func() { *otlptracehttp.MaxResponseBodySize = orig }) + + // largeBody is larger than the 1-byte limit. + largeBody := []byte("xx") + + tests := []struct { + name string + status int + contentType string + }{ + { + name: "success response body too large", + status: http.StatusOK, + contentType: "application/x-protobuf", + }, + { + name: "error response body too large", + status: http.StatusServiceUnavailable, + contentType: "text/plain", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var calls int + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + calls++ + w.Header().Set("Content-Type", tc.contentType) + w.WriteHeader(tc.status) + _, _ = w.Write(largeBody) + })) + t.Cleanup(srv.Close) + + client := otlptracehttp.NewClient( + otlptracehttp.WithEndpointURL(srv.URL), + otlptracehttp.WithInsecure(), + otlptracehttp.WithRetry(otlptracehttp.RetryConfig{Enabled: false}), + ) + exporter, err := otlptrace.New(t.Context(), client) + require.NoError(t, err) + t.Cleanup(func() { _ = exporter.Shutdown(t.Context()) }) + + err = exporter.ExportSpans(t.Context(), otlptracetest.SingleReadOnlySpan()) + assert.ErrorContains(t, err, "response body too large") + assert.Equal(t, 1, calls, "request must not be retried after body-too-large error") + }) + } +} + func TestGetBodyCalledOnRedirect(t *testing.T) { // Test that req.GetBody is set correctly, allowing the HTTP transport // to re-send the body on 307 redirects.
exporters/otlp/otlptrace/otlptracehttp/export_test.go+8 −0 added@@ -0,0 +1,8 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package otlptracehttp + +// MaxResponseBodySize exposes the package-level maxResponseBodySize variable +// to allow tests to override it. +var MaxResponseBodySize = &maxResponseBodySize
Vulnerability mechanics
Root cause
"Missing size cap on HTTP response body read in OTLP HTTP exporters allows an attacker to cause memory exhaustion by returning an arbitrarily large response."
Attack vector
An attacker who controls the OTLP collector endpoint configured in the exporter, or who can perform a man-in-the-middle attack on the network path between the exporter and the collector, sends an HTTP response with a very large body. The exporter reads the entire response into memory without a size limit, causing heap memory exhaustion and potentially crashing the instrumented process (denial of service). The CVSS vector reflects a medium-severity attack requiring adjacent network access and high attack complexity [ref_id=1].
Affected code
In the C++ library (`opentelemetry-cpp`), the three OTLP HTTP exporters (traces, metrics, logs) in `ext/src/http/client/curl/http_operation_curl.cc` read the full HTTP response into an in-memory vector without a size cap [patch_id=5723734]. In the Go library (`opentelemetry-go`), the same vulnerability exists in `exporters/otlp/otlptrace/otlptracehttp/client.go`, `exporters/otlp/otlpmetric/otlpmetrichttp/client.go`, and `exporters/otlp/otlplog/otlploghttp/client.go`, where `io.Copy(&respData, resp.Body)` is called with no upper bound [patch_id=5723735][ref_id=1].
What the fix does
In the C++ fix [patch_id=5723734], a `max_response_size_` field (defaulting to 4 MiB) is added to `HttpOperation`, and each write callback (`WriteMemoryCallback`, `WriteVectorHeaderCallback`, `WriteVectorBodyCallback`) now checks whether the incoming chunk would exceed the remaining budget before appending to the internal vector. In the Go fix [patch_id=5723735], each `io.Copy` call is replaced with `io.Copy` through `http.MaxBytesReader(nil, resp.Body, maxResponseBodySize)`, where `maxResponseBodySize` is set to 4 MiB. When the limit is exceeded, a `*http.MaxBytesError` is returned and surfaced as a non-retryable error, preventing unbounded memory allocation.
Preconditions
- networkThe exporter must be configured to send telemetry to an attacker-controlled collector endpoint, or the attacker must be able to MITM the connection between the exporter and a legitimate collector.
- authNo authentication or special privileges are required; the attack is triggered purely by the HTTP response body.
Generated on Jun 12, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.