From 5d695516bac3e2cd3efb2abc2c619b444a62fa2d Mon Sep 17 00:00:00 2001 From: Julien Pivotto <291750+roidelapluie@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:38:01 +0200 Subject: [PATCH] storage/remote: add OTLP request body read limit Apply the same io.LimitReader guard (decodeReadLimit = 32 MiB) to the OTLP write decoder that remote read already use, so that a gzip-encoded request body cannot decompress to unbounded memory. Signed-off-by: Julien Pivotto <291750+roidelapluie@users.noreply.github.com> --- storage/remote/codec.go | 2 +- storage/remote/codec_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/storage/remote/codec.go b/storage/remote/codec.go index c689a51164..695bf196a8 100644 --- a/storage/remote/codec.go +++ b/storage/remote/codec.go @@ -1000,7 +1000,7 @@ func DecodeOTLPWriteRequest(r *http.Request) (pmetricotlp.ExportRequest, error) return pmetricotlp.NewExportRequest(), fmt.Errorf("unsupported compression: %s. Only \"gzip\" or no compression supported", r.Header.Get("Content-Encoding")) } - body, err := io.ReadAll(reader) + body, err := io.ReadAll(io.LimitReader(reader, decodeReadLimit)) if err != nil { r.Body.Close() return pmetricotlp.NewExportRequest(), err diff --git a/storage/remote/codec_test.go b/storage/remote/codec_test.go index 5da8c8176c..196b702d16 100644 --- a/storage/remote/codec_test.go +++ b/storage/remote/codec_test.go @@ -15,9 +15,12 @@ package remote import ( "bytes" + "compress/gzip" "errors" "fmt" "io" + "net/http" + "strings" "sync" "testing" @@ -25,6 +28,8 @@ import ( "github.com/prometheus/common/model" "github.com/prometheus/common/promslog" "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/pdata/pmetric/pmetricotlp" "github.com/prometheus/prometheus/config" "github.com/prometheus/prometheus/model/histogram" @@ -729,6 +734,37 @@ func TestMergeLabels(t *testing.T) { } } +func TestDecodeOTLPWriteRequestGzipSizeLimit(t *testing.T) { + // Build a valid OTLP request whose serialized protobuf exceeds decodeReadLimit. + // A metric description filled with repeated characters compresses very + // efficiently, so the gzip payload is small while the decompressed form is + // larger than the 32 MiB limit. + d := pmetric.NewMetrics() + m := d.ResourceMetrics().AppendEmpty().ScopeMetrics().AppendEmpty().Metrics().AppendEmpty() + m.SetName("test_metric") + m.SetDescription(strings.Repeat("a", decodeReadLimit+1)) + m.SetEmptyGauge() + + proto, err := pmetricotlp.NewExportRequestFromMetrics(d).MarshalProto() + require.NoError(t, err) + + var buf bytes.Buffer + gz := gzip.NewWriter(&buf) + _, err = gz.Write(proto) + require.NoError(t, err) + require.NoError(t, gz.Close()) + + req, err := http.NewRequest(http.MethodPost, "/", &buf) + require.NoError(t, err) + req.Header.Set("Content-Type", pbContentType) + req.Header.Set("Content-Encoding", "gzip") + + // The decompressed payload exceeds decodeReadLimit and is truncated, so the + // protobuf cannot be parsed into a valid ExportRequest. + _, err = DecodeOTLPWriteRequest(req) + require.Error(t, err) +} + func TestDecodeWriteRequest(t *testing.T) { buf, _, _, err := buildWriteRequest(nil, writeRequestFixture.Timeseries, nil, nil, nil, nil, "snappy") require.NoError(t, err)