VYPR
Medium severity5.3GHSA Advisory· Published Jun 15, 2026· Updated Jun 15, 2026

OpenTelemetry Core: Unbounded memory allocation in W3C Baggage propagation

CVE-2026-54285

Description

@opentelemetry/core before 2.8.0 does not enforce baggage size limits on inbound headers, allowing potential denial of service via memory exhaustion.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

@opentelemetry/core before 2.8.0 does not enforce baggage size limits on inbound headers, allowing potential denial of service via memory exhaustion.

Vulnerability

The W3CBaggagePropagator.extract() method in @opentelemetry/core fails to enforce size limits when parsing inbound baggage HTTP headers. The W3C Baggage specification recommends a maximum total size of 8,192 bytes and 180 entries, but these limits were only applied on the outbound (inject()) path, not on the inbound (extract()) path. Affected versions are all prior to 2.8.0 [1][2].

Exploitation

An attacker can send an HTTP request containing an oversized baggage header (e.g., exceeding the recommended limits). The propagator parses the header without any size cap, allocating memory proportional to the header size. For HTTP transports, the default Node.js --max-http-header-size of 16,384 bytes constrains the maximum deliverable payload, but this restriction can be circumvented in deployments that raise the limit or use non-HTTP transports (e.g., messaging systems, custom TextMapGetter implementations) where no transport-layer limit exists [1][2]. No authentication or user interaction is required.

Impact

The vulnerability results in memory allocation proportional to the header size without a cap, potentially exhausting available memory and causing a denial of service. In typical Node.js deployments with default HTTP header size limits, the practical impact is limited because the header is already in memory before reaching the propagator and the additional allocation is only the overhead of splitting into entry objects [1][2]. However, risk increases significantly when transport-layer limits are absent or elevated.

Mitigation

Update @opentelemetry/core to version 2.8.0 or later, where the fix enforces limits consistent with the W3C specification: maximum total baggage size 8,192 bytes, maximum entries 180, and maximum per-entry size 4,096 bytes. Headers exceeding these limits are truncated at the point the limit is reached [1][2]. As a workaround, ensure header size limits are configured at the server or gateway level; the default Node.js HTTP header limit (16 KB) provides partial mitigation. For non-HTTP transports receiving baggage from untrusted sources, validate input size before passing it to the propagator [1][2].

AI Insight generated on Jun 15, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

1
4b13587d1e08

Merge commit from fork

https://github.com/open-telemetry/opentelemetry-jsMarc PichlerJun 11, 2026Fixed in 2.8.0via ghsa-release-walk
4 files changed · +158 20
  • CHANGELOG.md+2 0 modified
    @@ -20,6 +20,8 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2
     
     ### :bug: Bug Fixes
     
    +* fix(core): limit processing of incoming "baggage" header to 8192 bytes @pichlermarc
    +
     ### :books: Documentation
     
     ### :house: Internal
    
  • packages/opentelemetry-core/src/baggage/propagation/W3CBaggagePropagator.ts+30 19 modified
    @@ -15,11 +15,14 @@ import { propagation } from '@opentelemetry/api';
     import { isTracingSuppressed } from '../../trace/suppress-tracing';
     import {
       BAGGAGE_HEADER,
    -  BAGGAGE_ITEMS_SEPARATOR,
       BAGGAGE_MAX_NAME_VALUE_PAIRS,
       BAGGAGE_MAX_PER_NAME_VALUE_PAIRS,
     } from '../constants';
    -import { getKeyPairs, parsePairKeyValue, serializeKeyPairs } from '../utils';
    +import {
    +  getKeyPairs,
    +  parseBaggageHeaderString,
    +  serializeKeyPairs,
    +} from '../utils';
     
     /**
      * Propagates {@link Baggage} through Context format propagation.
    @@ -44,28 +47,36 @@ export class W3CBaggagePropagator implements TextMapPropagator {
     
       extract(context: Context, carrier: unknown, getter: TextMapGetter): Context {
         const headerValue = getter.get(carrier, BAGGAGE_HEADER);
    -    const baggageString = Array.isArray(headerValue)
    -      ? headerValue.join(BAGGAGE_ITEMS_SEPARATOR)
    -      : headerValue;
    -    if (!baggageString) return context;
    -    const baggage: Record<string, BaggageEntry> = {};
    -    if (baggageString.length === 0) {
    +    if (!headerValue) {
           return context;
         }
    -    const pairs = baggageString.split(BAGGAGE_ITEMS_SEPARATOR);
    -    pairs.forEach(entry => {
    -      const keyPair = parsePairKeyValue(entry);
    -      if (keyPair) {
    -        const baggageEntry: BaggageEntry = { value: keyPair.value };
    -        if (keyPair.metadata) {
    -          baggageEntry.metadata = keyPair.metadata;
    -        }
    -        baggage[keyPair.key] = baggageEntry;
    +
    +    const baggage: Record<string, BaggageEntry> = {};
    +    let count = 0;
    +
    +    let totalSize = 0;
    +    if (Array.isArray(headerValue)) {
    +      for (let i = 0; i < headerValue.length; i++) {
    +        [count, totalSize] = parseBaggageHeaderString(
    +          headerValue[i],
    +          baggage,
    +          count,
    +          totalSize
    +        );
           }
    -    });
    -    if (Object.entries(baggage).length === 0) {
    +    } else {
    +      [count] = parseBaggageHeaderString(
    +        headerValue,
    +        baggage,
    +        count,
    +        totalSize
    +      );
    +    }
    +
    +    if (count === 0) {
           return context;
         }
    +
         return propagation.setBaggage(context, propagation.createBaggage(baggage));
       }
     
    
  • packages/opentelemetry-core/src/baggage/utils.ts+44 1 modified
    @@ -2,13 +2,19 @@
      * Copyright The OpenTelemetry Authors
      * SPDX-License-Identifier: Apache-2.0
      */
    -import type { Baggage, BaggageEntryMetadata } from '@opentelemetry/api';
    +import type {
    +  Baggage,
    +  BaggageEntry,
    +  BaggageEntryMetadata,
    +} from '@opentelemetry/api';
     import { baggageEntryMetadataFromString } from '@opentelemetry/api';
     import {
       BAGGAGE_ITEMS_SEPARATOR,
       BAGGAGE_PROPERTIES_SEPARATOR,
       BAGGAGE_KEY_PAIR_SEPARATOR,
       BAGGAGE_MAX_TOTAL_LENGTH,
    +  BAGGAGE_MAX_NAME_VALUE_PAIRS,
    +  BAGGAGE_MAX_PER_NAME_VALUE_PAIRS,
     } from './constants';
     
     type ParsedBaggageKeyValue = {
    @@ -78,6 +84,43 @@ export function parsePairKeyValue(
       return { key, value, metadata };
     }
     
    +/**
    + * Parses a single baggage header string into the provided record, applying limits defined in this package.
    + * Uses indexOf/substring in a while loop to avoid allocating a full array of split entries.
    + * Returns the updated pair count so callers can track totals across multiple header values.
    + */
    +export function parseBaggageHeaderString(
    +  value: string,
    +  baggage: Record<string, BaggageEntry>,
    +  count: number,
    +  totalSize: number
    +): [count: number, totalSize: number] {
    +  let start = 0;
    +  while (start < value.length && count < BAGGAGE_MAX_NAME_VALUE_PAIRS) {
    +    const end = value.indexOf(BAGGAGE_ITEMS_SEPARATOR, start);
    +    const entryEnd = end === -1 ? value.length : end;
    +    const entryLength = entryEnd - start;
    +
    +    if (entryLength <= BAGGAGE_MAX_PER_NAME_VALUE_PAIRS) {
    +      const keyPair = parsePairKeyValue(value.substring(start, entryEnd));
    +      if (keyPair) {
    +        // Comma separator is counted for every accepted entry after the first
    +        const entrySize = (count === 0 ? 0 : 1) + entryLength;
    +        if (totalSize + entrySize > BAGGAGE_MAX_TOTAL_LENGTH) break;
    +        baggage[keyPair.key] = keyPair.metadata
    +          ? { value: keyPair.value, metadata: keyPair.metadata }
    +          : { value: keyPair.value };
    +        count++;
    +        totalSize += entrySize;
    +      }
    +    }
    +
    +    if (end === -1) break;
    +    start = end + 1;
    +  }
    +  return [count, totalSize];
    +}
    +
     /**
      * Parse a string serialized in the baggage HTTP Format (without metadata):
      * https://github.com/w3c/baggage/blob/master/baggage/HTTP_HEADER_FORMAT.md
    
  • packages/opentelemetry-core/test/common/baggage/W3CBaggagePropagator.test.ts+82 0 modified
    @@ -218,6 +218,88 @@ describe('W3CBaggagePropagator', () => {
     
           assert.deepStrictEqual(extractedBaggage, expected);
         });
    +
    +    it('should not extract more than 180 pairs from a string header', function () {
    +      const pairs = Array.from({ length: 200 }, (_, i) => `k${i}=v`);
    +      carrier[BAGGAGE_HEADER] = pairs.join(',');
    +      const bag = propagation.getBaggage(
    +        httpBaggagePropagator.extract(
    +          ROOT_CONTEXT,
    +          carrier,
    +          defaultTextMapGetter
    +        )
    +      );
    +      assert.ok(bag);
    +      assert.strictEqual(bag.getAllEntries().length, 180);
    +    });
    +
    +    it('should not extract more than 180 pairs across multiple array header values', function () {
    +      const first = Array.from({ length: 100 }, (_, i) => `a${i}=v`).join(',');
    +      const second = Array.from({ length: 100 }, (_, i) => `b${i}=v`).join(',');
    +      carrier[BAGGAGE_HEADER] = [first, second];
    +      const bag = propagation.getBaggage(
    +        httpBaggagePropagator.extract(
    +          ROOT_CONTEXT,
    +          carrier,
    +          defaultTextMapGetter
    +        )
    +      );
    +      assert.ok(bag);
    +      assert.strictEqual(bag.getAllEntries().length, 180);
    +    });
    +
    +    it('should stop accepting entries once the extracted baggage exceeds 8192 bytes', function () {
    +      // Each entry is 49 bytes ("kXXX=v...v" with 45 v's).
    +      // Accepted-size accounting: first entry = 49, each subsequent = 1 (comma) + 49 = 50.
    +      // After n entries: totalSize = 49 + (n-1)*50 = 50n - 1.
    +      // The 164th entry would bring totalSize to 50*163 - 1 + 50 = 8199 > 8192, so we stop at 163
    +      // — below the 180 pair limit, proving the extracted-size limit fires first.
    +      const value = 'v'.repeat(45);
    +      const pairs = Array.from(
    +        { length: 200 },
    +        (_, i) => `${String(i).padStart(3, '0')}=${value}`
    +      );
    +      carrier[BAGGAGE_HEADER] = pairs.join(',');
    +      const bag = propagation.getBaggage(
    +        httpBaggagePropagator.extract(
    +          ROOT_CONTEXT,
    +          carrier,
    +          defaultTextMapGetter
    +        )
    +      );
    +      assert.ok(bag);
    +      assert.strictEqual(bag.getAllEntries().length, 163);
    +    });
    +
    +    it('should reject a header that is a single entry exceeding 4096 bytes', function () {
    +      const longValue = 'v'.repeat(4093); // key=<longValue> = 4+1+4093 = 4098 bytes > 4096
    +      carrier[BAGGAGE_HEADER] = `toolong=${longValue}`;
    +      const bag = propagation.getBaggage(
    +        httpBaggagePropagator.extract(
    +          ROOT_CONTEXT,
    +          carrier,
    +          defaultTextMapGetter
    +        )
    +      );
    +      assert.strictEqual(bag, undefined);
    +    });
    +
    +    it('should skip pairs exceeding 4096 bytes but still extract valid peers', function () {
    +      const longValue = 'v'.repeat(4093); // key=<longValue> = 4+1+4093 = 4098 bytes > 4096
    +      carrier[BAGGAGE_HEADER] = `good=value,toolong=${longValue},also=good`;
    +      const bag = propagation.getBaggage(
    +        httpBaggagePropagator.extract(
    +          ROOT_CONTEXT,
    +          carrier,
    +          defaultTextMapGetter
    +        )
    +      );
    +      assert.ok(bag);
    +      assert.strictEqual(bag.getAllEntries().length, 2);
    +      assert.ok(bag.getEntry('good'));
    +      assert.ok(bag.getEntry('also'));
    +      assert.strictEqual(bag.getEntry('toolong'), undefined);
    +    });
       });
     
       describe('fields()', () => {
    

Vulnerability mechanics

No source-code context for this CVE — mechanics is only generated when we can read the actual fix diff. Without that, the four sections (root cause, attack vector, affected code, fix) would be speculation rather than analysis.

References

2

News mentions

0

No linked articles in our index yet.