From 814f377e58a6b638d9ec9bb811a193c0886b9cc1 Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Wed, 27 May 2026 09:41:59 +0800 Subject: [PATCH] fix(api): apply baggage size and entry caps on extract The W3C Baggage TextMapPropagator declares three constants (MAX_ENTRIES=180, MAX_ENTRY_LENGTH=4096, MAX_TOTAL_LENGTH=8192) and the private 'encode' helper applies all three on the inject path. The 'extract' path walks the inbound header through 'gsub' and 'split' and iterates every entry with no corresponding gate, so the caps are asymmetric between the two directions. This change hoists the same policy into 'extract' so the two paths are consistent: - reject the header outright when its byte size exceeds MAX_TOTAL_LENGTH - skip any individual entry whose byte size exceeds MAX_ENTRY_LENGTH - stop iterating after MAX_ENTRIES decoded entries The loop body is factored into a private 'decode_entries' helper to keep 'extract' within the project's complexity budget. Three new tests cover each of the gates and mirror the existing inject-side limit tests. Signed-off-by: tonghuaroot --- .../propagation/text_map_propagator.rb | 26 +++++++++----- .../propagation/text_map_propagator_test.rb | 35 +++++++++++++++++++ 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/api/lib/opentelemetry/baggage/propagation/text_map_propagator.rb b/api/lib/opentelemetry/baggage/propagation/text_map_propagator.rb index 0197c2f9c3..0f58318465 100644 --- a/api/lib/opentelemetry/baggage/propagation/text_map_propagator.rb +++ b/api/lib/opentelemetry/baggage/propagation/text_map_propagator.rb @@ -54,18 +54,12 @@ def inject(carrier, context: Context.current, setter: Context::Propagation.text_ def extract(carrier, context: Context.current, getter: Context::Propagation.text_map_getter) header = getter.get(carrier, BAGGAGE_KEY) return context if header.nil? || header.empty? + return context if header.bytesize > MAX_TOTAL_LENGTH entries = header.gsub(/\s/, '').split(',') OpenTelemetry::Baggage.build(context: context) do |builder| - entries.each do |entry| - # Note metadata is currently unused in OpenTelemetry, but is part - # the W3C spec where it's referred to as properties. We preserve - # the properties (as-is) so that they can be propagated elsewhere. - kv, meta = entry.split(';', 2) - k, v = kv.split('=').map!(&URI.method(:decode_uri_component)) - builder.set_value(k, v, metadata: meta) - end + decode_entries(entries, builder) end rescue StandardError => e OpenTelemetry.logger.debug "Error extracting W3C baggage: #{e.message}" @@ -82,6 +76,22 @@ def fields private + def decode_entries(entries, builder) + decoded_count = 0 + entries.each do |entry| + break if decoded_count >= MAX_ENTRIES + next if entry.bytesize > MAX_ENTRY_LENGTH + + # Note metadata is currently unused in OpenTelemetry, but is part + # the W3C spec where it's referred to as properties. We preserve + # the properties (as-is) so that they can be propagated elsewhere. + kv, meta = entry.split(';', 2) + k, v = kv.split('=').map!(&URI.method(:decode_uri_component)) + builder.set_value(k, v, metadata: meta) + decoded_count += 1 + end + end + def encode(baggage) result = +'' encoded_count = 0 diff --git a/api/test/opentelemetry/baggage/propagation/text_map_propagator_test.rb b/api/test/opentelemetry/baggage/propagation/text_map_propagator_test.rb index 9d5187cea9..03359b4502 100644 --- a/api/test/opentelemetry/baggage/propagation/text_map_propagator_test.rb +++ b/api/test/opentelemetry/baggage/propagation/text_map_propagator_test.rb @@ -70,6 +70,41 @@ _(context.object_id).wont_equal(empty_context.object_id) end end + + describe 'limits mirroring #inject' do + it 'returns the same context object when the header exceeds the total length of 8192 bytes' do + header = (['k=' + ('v' * 96)] * 100).join(',') + _(header.bytesize).must_be :>, 8192 + carrier = { header_key => header } + empty_context = Context.empty + context = propagator.extract(carrier, context: empty_context) + _(context.object_id).must_equal(empty_context.object_id) + end + + it 'enforces max of 180 entries on extract' do + header = (0..199).map { |i| "k#{i}=v#{i}" }.join(',') + carrier = { header_key => header } + context = propagator.extract(carrier, context: Context.empty) + entries = OpenTelemetry::Baggage.raw_entries(context: context) + _(entries.size).must_equal(180) + _(entries['k0']).wont_be_nil + _(entries['k179']).wont_be_nil + _(entries['k180']).must_be_nil + _(entries['k199']).must_be_nil + end + + it 'skips entries whose size exceeds the max entry length of 4096 bytes' do + oversize_value = 'v' * 4096 + header = "big=#{oversize_value},key2=val2" + _(header.bytesize).must_be :<=, 8192 + carrier = { header_key => header } + context = propagator.extract(carrier, context: Context.empty) + entries = OpenTelemetry::Baggage.raw_entries(context: context) + _(entries['big']).must_be_nil + _(entries['key2']).wont_be_nil + _(entries['key2'].value).must_equal('val2') + end + end end describe '#inject' do