Summary
otel_propagator_baggage:extract/5 walks the inbound baggage HTTP header through string:lexemes(String, [$,]) and lists:foldl/3 without any byte or entry-count cap. Each entry is percent-decoded into a fresh binary and inserted into the accumulator map. The W3C Baggage specification recommends 8192 bytes and 180 entries; this propagator enforces neither.
This is a spec-compliance bug and a BEAM hardening item: an oversized baggage header (legitimately malformed or not) causes the calling process to spend an unbounded amount of scheduler time and per-process heap on the parse. The default propagator configuration {text_map_propagators, [trace_context, baggage]} registers otel_propagator_baggage on every instrumented HTTP entry point, so any Cowboy/Elli/Mochiweb service with OpenTelemetry trace propagation enabled inherits the same shape.
The maintainers reviewed this off-list as GHSA-64w2-whjg-q7q7 and declined to treat it as a security advisory, but asked for it to be opened as a regular issue/PR (https://github.com/open-telemetry/opentelemetry-erlang/security/advisories/GHSA-64w2-whjg-q7q7).
Reference: other OpenTelemetry SDKs already enforce these caps
| SDK |
File |
Byte cap |
Entry cap |
| Java SDK 1.62.0 |
W3CBaggagePropagator |
8192 |
180 |
| Go |
propagation/baggage.go |
8192 (maxBytes) |
64 (maxMembers) |
| .NET |
BaggagePropagator.cs |
8192 (MaxBaggageLength) |
180 (MaxBaggageItems) |
| C++ |
baggage.h FromHeader |
8192 (kMaxSize) |
180 (kMaxKeyValuePairs) |
| Erlang |
otel_propagator_baggage.erl |
none |
none |
Current code
apps/opentelemetry_api/src/otel_propagator_baggage.erl lines 81-94:
extract(Ctx, Carrier, _CarrierKeysFun, CarrierGet, _Options) ->
case CarrierGet(?BAGGAGE_HEADER, Carrier) of
undefined ->
Ctx;
String ->
Pairs = string:lexemes(String, [$,]),
DecodedBaggage =
lists:foldl(fun(Pair, Acc) ->
[Key, Value] = string:split(Pair, "="),
Acc#{decode_key(Key) => decode_value(Value)}
end, #{}, Pairs),
otel_baggage:set_to(Ctx, DecodedBaggage)
end.
string:lexemes(String, [$,]) allocates the full list of pair-binaries up front, lists:foldl then walks every pair and calls decode_key / decode_value (each builds a fresh binary), and the accumulator map keeps growing. None of byte_size(String), length(Pairs), or accumulator size is bounded. The [Key, Value] = string:split(...) match is also non-exhaustive: a single malformed pair (no =) crashes the extract.
Observed behavior
Tested against opentelemetry_api 1.4.0 installed from Hex.pm into a fresh rebar3 project on Erlang/OTP 27 erts-15.2.7.8:
N=1000 (9.8 KB header) extract: 35.7 ms mem 426 KB
N=50000 (660 KB header) extract: 33.6 seconds mem 22 MB
N=100000 (1.4 MB header) no output before 120s timeout
A single header thus consumes tens of seconds of scheduler time and tens of megabytes of per-process heap; repeated requests scale that linearly. Wrapping the call in a separate process does not help because the work is CPU-bound inside lists:foldl.
Suggested fix
Mirror the Java SDK 1.62.0 caps (MAX_BAGGAGE_BYTES = 8192, MAX_BAGGAGE_ENTRIES = 180):
- Reject headers larger than 8192 bytes before
string:lexemes/2 walks them.
- Stop decoding after 180 entries regardless of how many pairs
lexemes produced.
- While there, replace the non-exhaustive
[Key, Value] = string:split(Pair, "=") match with a case that skips malformed pairs instead of crashing the extract.
PR with that change and a otel_propagator_baggage_SUITE covering both caps is filed alongside this issue.
Summary
otel_propagator_baggage:extract/5walks the inboundbaggageHTTP header throughstring:lexemes(String, [$,])andlists:foldl/3without any byte or entry-count cap. Each entry is percent-decoded into a fresh binary and inserted into the accumulator map. The W3C Baggage specification recommends 8192 bytes and 180 entries; this propagator enforces neither.This is a spec-compliance bug and a BEAM hardening item: an oversized
baggageheader (legitimately malformed or not) causes the calling process to spend an unbounded amount of scheduler time and per-process heap on the parse. The default propagator configuration{text_map_propagators, [trace_context, baggage]}registersotel_propagator_baggageon every instrumented HTTP entry point, so any Cowboy/Elli/Mochiweb service with OpenTelemetry trace propagation enabled inherits the same shape.The maintainers reviewed this off-list as GHSA-64w2-whjg-q7q7 and declined to treat it as a security advisory, but asked for it to be opened as a regular issue/PR (https://github.com/open-telemetry/opentelemetry-erlang/security/advisories/GHSA-64w2-whjg-q7q7).
Reference: other OpenTelemetry SDKs already enforce these caps
W3CBaggagePropagatorpropagation/baggage.gomaxBytes)maxMembers)BaggagePropagator.csMaxBaggageLength)MaxBaggageItems)baggage.hFromHeaderkMaxSize)kMaxKeyValuePairs)otel_propagator_baggage.erlCurrent code
apps/opentelemetry_api/src/otel_propagator_baggage.erllines 81-94:string:lexemes(String, [$,])allocates the full list of pair-binaries up front,lists:foldlthen walks every pair and callsdecode_key/decode_value(each builds a fresh binary), and the accumulator map keeps growing. None ofbyte_size(String),length(Pairs), or accumulator size is bounded. The[Key, Value] = string:split(...)match is also non-exhaustive: a single malformed pair (no=) crashes the extract.Observed behavior
Tested against
opentelemetry_api 1.4.0installed from Hex.pm into a fresh rebar3 project on Erlang/OTP 27 erts-15.2.7.8:A single header thus consumes tens of seconds of scheduler time and tens of megabytes of per-process heap; repeated requests scale that linearly. Wrapping the call in a separate process does not help because the work is CPU-bound inside
lists:foldl.Suggested fix
Mirror the Java SDK 1.62.0 caps (
MAX_BAGGAGE_BYTES = 8192,MAX_BAGGAGE_ENTRIES = 180):string:lexemes/2walks them.lexemesproduced.[Key, Value] = string:split(Pair, "=")match with acasethat skips malformed pairs instead of crashing the extract.PR with that change and a
otel_propagator_baggage_SUITEcovering both caps is filed alongside this issue.