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>
This commit is contained in:
Julien Pivotto 2026-03-31 10:38:01 +02:00
parent d6c18a4d62
commit 5d695516ba
2 changed files with 37 additions and 1 deletions

View file

@ -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

View file

@ -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)