Oct 28, 2026·10 min read·3 visits
OpenTelemetry Python's opentelemetry-propagator-jaeger package prior to version 1.45.0 fails to enforce entry count or byte-length boundaries when parsing 'uberctx-' baggage headers. Remote attackers can send oversized or numerous baggage keys to exhaust CPU and memory resources, leading to denial of service. Version 1.45.0 resolves this by applying W3C Baggage limits (180 entries, 4,096 bytes per entry, and 8,192 bytes total) to both extraction and injection routines.
CVE-2026-116694 (GHSA-f5m9-jfrj-jjj7) is an unbounded resource allocation vulnerability (CWE-770) in the optional Jaeger propagator component (opentelemetry-propagator-jaeger) of the OpenTelemetry Python SDK prior to version 1.45.0. When extracting distributed tracing context from inbound carriers, JaegerPropagator.extract() processes all headers prefixed with 'uberctx-' without enforcing limits on entry count, per-entry byte size, or aggregate byte size. In environments where transport-level header constraints are absent, relaxed, or bypassed (such as non-HTTP message brokers, RPC frameworks, or web servers with elevated buffer configurations), an unauthenticated remote attacker can supply high-volume or oversized baggage headers to trigger excessive URL unquoting, short-lived immutable dictionary allocations, and CPU starvation.
OpenTelemetry Python provides distributed context propagation mechanisms that allow telemetry metadata—such as trace identifiers and application-defined key-value pairs known as baggage—to traverse process boundaries. Within this ecosystem, the optional opentelemetry-propagator-jaeger package implements the legacy Jaeger native propagation protocol (JaegerPropagator). While the default OpenTelemetry configuration uses W3C Trace Context and W3C Baggage formats, applications integrating with older Jaeger instrumentation or mixed microservice estates explicitly configure JaegerPropagator to extract and inject trace state.
In the Jaeger propagation specification, trace identifiers are carried in the uber-trace-id header, while baggage items are transmitted as separate carrier entries prefixed with uberctx- (for example, uberctx-user-id). Unlike the W3C Baggage specification, which mandates strict limits on the number of list members and total byte length, the historical Jaeger propagation format did not specify upper bounds on baggage volume. Consequently, prior to version 1.45.0, the JaegerPropagator implementation in opentelemetry-propagator-jaeger accepted and parsed arbitrary quantities of uberctx- entries of any length.
Classified under CWE-770 (Allocation of Resources Without Limits or Throttling), this flaw exposes services using JaegerPropagator to resource exhaustion. When an instrumented service processes an inbound carrier containing thousands of uberctx- keys or multi-megabyte values, the propagator performs unbounded string manipulation, URL percent-decoding, and repeated context dictionary copies. When front-end transport layers do not restrict metadata size, unauthenticated callers can leverage this behavior to degrade service availability through memory pressure and CPU saturation.
The root cause of CVE-2026-116694 resides in the _extract_baggage and inject methods of the JaegerPropagator class, located in propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py. During inbound context extraction, JaegerPropagator.extract() delegates baggage handling to _extract_baggage(), passing the caller-supplied carrier object and its associated Getter implementation.
In versions prior to 1.45.0, _extract_baggage() executed a list comprehension over the entire key space returned by getter.keys(carrier), filtering for any key beginning with self.BAGGAGE_PREFIX ("uberctx-"). This initial step forced full evaluation of all matching keys in the carrier without an upper limit. If a carrier contained 10,000 keys prefixed with uberctx-, the list comprehension allocated a 10,000-element list and proceeded to iterate over every entry sequentially.
Within the extraction loop, each header value was retrieved via getter.get(carrier, key) and passed directly to urllib.parse.unquote(value).strip(). Because no per-entry or cumulative byte limit existed, passing a multi-megabyte string forced the Python runtime to scan the entire string buffer, decode percent-encoded sequences, and allocate new string objects on the heap. This string processing occurred synchronously on the request-handling thread or event loop.
Compounding the string allocation overhead is the design of OpenTelemetry's runtime Context and baggage API in Python. For every extracted key-value pair, _extract_baggage() invoked baggage.set_baggage(key, value, context=context). Because OpenTelemetry Context objects are immutable mappings, every call to set_baggage() instantiates a new Context object and copies the existing baggage dictionary to append the single new key. Processing $N$ baggage keys therefore results in $O(N^2)$ dictionary key copies and $N$ distinct heap allocations, amplifying garbage collection pressure. Furthermore, on outbound requests, JaegerPropagator.inject() iterated over all entries in the in-process baggage map without bounds, propagating oversized context state to downstream services.
The vulnerability was remediated in Pull Request #5556 (commit 65c4eeb87cd3815dc795d3b5485e9ea0d5affcc7). Prior to this commit, the baggage extraction and injection routines in opentelemetry/propagators/jaeger/__init__.py contained no cardinality or byte-size checks:
# VULNERABLE CODE (Prior to 1.45.0)
def inject(
self,
carrier: CarrierT,
context: typing.Optional[Context] = None,
setter: Setter[CarrierT] = default_setter,
) -> None:
# ... [trace context injection omitted] ...
baggage_entries = baggage.get_all(context=context)
if not baggage_entries:
return
# Unbounded iteration over all in-process baggage entries
for key, value in baggage_entries.items():
baggage_key = self.BAGGAGE_PREFIX + key
setter.set(carrier, baggage_key, urllib.parse.quote(str(value)))
def _extract_baggage(
self,
getter: Getter[CarrierT],
carrier: CarrierT,
context: Context,
) -> Context:
# Materializes all matching keys without an entry limit
baggage_keys = [key for key in getter.keys(carrier) if key.startswith(self.BAGGAGE_PREFIX)]
for key in baggage_keys:
value = _extract_first_element(getter.get(carrier, key))
if value is None:
continue
# Unbounded unquoting and O(N^2) immutable context re-allocation
context = baggage.set_baggage(
key.replace(self.BAGGAGE_PREFIX, ""),
urllib.parse.unquote(value).strip(),
context=context,
)
return contextTo bound resource consumption, the patch adopts the structural limits defined by the W3C Baggage specification—already enforced by OpenTelemetry's core W3CBaggagePropagator—and applies them to JaegerPropagator. Specifically, the class defines MAX_BAGGAGE_ENTRIES = 180, MAX_BAGGAGE_ENTRY_BYTES = 4096, and MAX_BAGGAGE_TOTAL_BYTES = 8192.
# PATCHED CODE (Version 1.45.0+)
class JaegerPropagator(TextMapPropagator):
TRACE_ID_KEY = "uber-trace-id"
BAGGAGE_PREFIX = "uberctx-"
DEBUG_FLAG = 0x02
MAX_BAGGAGE_ENTRIES = 180
MAX_BAGGAGE_ENTRY_BYTES = 4096
MAX_BAGGAGE_TOTAL_BYTES = 8192
def _extract_baggage(
self,
getter: Getter[CarrierT],
carrier: CarrierT,
context: Context,
) -> Context:
# Generator expression paired with itertools.islice caps candidate inspection
# at 180 keys, preventing CPU exhaustion from thousands of rejected keys.
candidates = itertools.islice(
(key for key in getter.keys(carrier) if key.startswith(self.BAGGAGE_PREFIX)),
self.MAX_BAGGAGE_ENTRIES,
)
pairs = []
for key in candidates:
value = _extract_first_element(getter.get(carrier, key))
if value is not None:
pairs.append((key.replace(self.BAGGAGE_PREFIX, ""), value))
# Filter pairs by per-entry and cumulative byte budgets before unquoting
for baggage_key, value in _limit_baggage_bytes(
pairs, self.MAX_BAGGAGE_ENTRY_BYTES, self.MAX_BAGGAGE_TOTAL_BYTES
):
context = baggage.set_baggage(
baggage_key,
urllib.parse.unquote(value).strip(),
context=context,
)
return context
def _inject_baggage(
self,
setter: Setter[CarrierT],
carrier: CarrierT,
baggage_entries: collections.abc.Mapping[str, object],
) -> None:
candidates = itertools.islice(baggage_entries.items(), self.MAX_BAGGAGE_ENTRIES)
pairs = [(key, urllib.parse.quote(str(value))) for key, value in candidates]
for key, encoded_value in _limit_baggage_bytes(
pairs, self.MAX_BAGGAGE_ENTRY_BYTES, self.MAX_BAGGAGE_TOTAL_BYTES
):
setter.set(carrier, self.BAGGAGE_PREFIX + key, encoded_value)
def _limit_baggage_bytes(
pairs: collections.abc.Iterable[tuple[str, str]],
max_entry_bytes: int,
max_total_bytes: int,
) -> collections.abc.Iterator[tuple[str, str]]:
total_bytes = 0
accepted = 0
for key, value in pairs:
# Measure UTF-8 encoded byte length, plus 1 byte for the '=' delimiter
entry_bytes = len(key.encode()) + len(value.encode()) + 1
if entry_bytes > max_entry_bytes:
continue
# Account for 1-byte ',' separator between multiple baggage items
separator_bytes = 1 if accepted > 0 else 0
if total_bytes + separator_bytes + entry_bytes > max_total_bytes:
continue
yield key, value
total_bytes += separator_bytes + entry_bytes
accepted += 1The patch addresses several subtle edge cases to ensure fix completeness:
_extract_baggage() replaces the list comprehension with a generator expression wrapped in itertools.islice(..., self.MAX_BAGGAGE_ENTRIES). Crucially, this bounds the number of candidates inspected rather than the number of valid entries retained. If the limit only counted retained entries, an attacker could send 50,000 oversized entries (each exceeding 4,096 bytes); all 50,000 would be rejected by the byte filter, forcing the loop to inspect and encode-check all 50,000 items._limit_baggage_bytes() evaluates len(key.encode()) + len(value.encode()) + 1 before calling urllib.parse.unquote(value).strip() or baggage.set_baggage(). Oversized values are discarded prior to URL decoding and context dictionary copying.len(key.encode()) rather than len(key) guarantees that multi-byte UTF-8 characters cannot exceed the 4,096-byte or 8,192-byte memory budgets.Exploiting CVE-2026-116694 requires two primary conditions: the target Python service must explicitly configure JaegerPropagator (via set_global_textmap(JaegerPropagator()) or instrumentation-specific propagator overrides, or via the OTEL_PROPAGATORS=jaeger environment variable), and the underlying transport layer must deliver oversized or high-count metadata fields to the application's TextMapGetter.
In standard HTTP deployments, web servers and reverse proxies enforce default header constraints that restrict the payload size reaching the Python WSGI or ASGI worker. For example, Gunicorn defaults to limit_request_fields = 100 and limit_request_field_size = 8190, while Uvicorn's h11 parser enforces a 16,384-byte incomplete event buffer limit. Under these default configurations, HTTP frontends block excessively large requests before JaegerPropagator.extract() is invoked.
However, practical exploitation becomes viable across several common architectural patterns where transport limits are relaxed or absent:
limit_request_field_size (or set it to 0 for unlimited) and increase Nginx's large_client_header_buffers.TextMapGetter. Message brokers typically permit multi-megabyte message headers or thousands of dictionary keys without HTTP-style field limits.TextMapGetter Implementations: Applications extracting context from WebSocket frames, database records, or custom RPC protocols pass raw Python dictionaries directly to JaegerPropagator.extract().The unit test suite committed alongside the patch in test_jaeger_propagator.py demonstrates the mechanics of triggering these boundary conditions:
# 1. High-cardinality key flood (exceeding 180 entries)
def test_extract_enforces_max_baggage_entries(self):
old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id}
for index in range(200):
old_carrier[f"uberctx-k{index}"] = f"v{index}"
# Unpatched versions extract all 200 items, performing 200 immutable Context copies.
# Patched versions cap extraction at 180 entries.
extracted = FORMAT.extract(old_carrier)[_BAGGAGE_KEY]
self.assertEqual(FORMAT.MAX_BAGGAGE_ENTRIES, len(extracted))
# 2. Oversized single entry (exceeding 4,096 bytes)
def test_extract_drops_oversized_baggage_entry(self):
old_carrier = {
FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id,
"uberctx-ok": "value",
"uberctx-big": "x" * 5000,
}
# Unpatched versions unquote and store the 5,000-byte payload.
# Patched versions drop "uberctx-big" prior to unquoting.
context = FORMAT.extract(old_carrier)
self.assertDictEqual({"ok": "value"}, context[_BAGGAGE_KEY])
# 3. Candidate inspection flood (1,000 oversized keys)
def test_extract_stops_inspecting_after_the_candidate_limit(self):
old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id}
for index in range(1000):
old_carrier[f"uberctx-k{index}"] = "x" * 5000
# Unpatched versions inspect, unquote, and copy all 1,000 keys (5 MB total).
# Patched versions halt getter evaluation after 180 candidates.CVE-2026-116694 is assigned a CVSS v4.0 base score of 6.3 (Medium) with the vector CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N. The Attack Vector is Network (AV:N), Attack Complexity is Low (AC:L), and neither privileges (PR:N) nor user interaction (UI:N) are required. Attack Requirements are marked as Present (AT:P) because exploitation depends on both the explicit configuration of the non-default JaegerPropagator and a transport path that permits large or high-count metadata headers.
The security impact is confined strictly to system availability (VA:L), with no impact to confidentiality (VC:N) or integrity (VI:N). In Python applications—particularly single-threaded asynchronous event loops (such as asyncio with FastAPI or aiohttp) or pre-fork worker models with limited worker counts—CPU time spent synchronously iterating over thousands of keys, decoding percent-encoded strings, and copying dictionaries blocks the handling of concurrent requests.
Additionally, if an unpatched service extracts a high volume of uberctx- baggage items into its active Context and subsequently performs fan-out RPC or HTTP requests to downstream microservices using JaegerPropagator.inject(), the service will serialize and transmit all accumulated uberctx- headers on every outbound call. This behavior causes downstream amplification, multiplying memory allocation and network serialization overhead across internal service meshes or causes outbound requests to fail when downstream servers reject the oversized headers.
The primary remediation is to upgrade opentelemetry-propagator-jaeger (and synchronized opentelemetry-python core packages) to version 1.45.0 or later. Version 1.45.0 enforces the 180-entry, 4,096-byte per-entry, and 8,192-byte total boundaries across both extract() and inject() methods without requiring configuration changes.
pip install --upgrade "opentelemetry-propagator-jaeger>=1.45.0"> [!NOTE]
> In 1.45.0+, baggage entries exceeding 4,096 bytes, entries that would push the cumulative size beyond 8,192 bytes, or candidates beyond the first 180 uberctx- keys are silently dropped during extraction and injection. Applications that legitimately rely on propagating payloads larger than 8 KB via Jaeger baggage must refactor their architecture, as distributed tracing headers are not designed for bulk data transport.
For deployments where immediate package upgrades are not possible, operators should implement the following compensating controls:
jaeger from the OTEL_PROPAGATORS environment variable or code configuration and use the default tracecontext,baggage (W3C Trace Context and W3C Baggage) propagators, which already enforce strict specification limits.large_client_header_buffers 4 8k; or lower; in Gunicorn, ensure limit_request_fields <= 100 and limit_request_field_size <= 8190.TextMapGetter classes, implement a wrapper around getter.keys() and getter.get() that truncates uberctx- keys at 180 entries and drops values exceeding 4,096 bytes before invoking JaegerPropagator.extract().CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
opentelemetry-propagator-jaeger OpenTelemetry | < 1.45.0 | 1.45.0 |
| Attribute | Detail |
|---|---|
| CVE / Advisory ID | CVE-2026-116694 / GHSA-f5m9-jfrj-jjj7 |
| Affected Package | opentelemetry-propagator-jaeger (< 1.45.0) |
| Weakness Class | CWE-770 (Allocation of Resources Without Limits or Throttling) |
| CVSS v4.0 Score | 6.3 Medium (CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N) |
| Attack Vector | Network via inbound 'uberctx-*' carrier headers/metadata |
| Security Impact | Denial of Service via CPU exhaustion and repeated heap allocations |
| Exploit Status | Unit test reproduction in official repository; no wild exploitation reported |
| Patched Version | 1.45.0 (Enforces 180 entries, 4,096B/entry, 8,192B total) |
The software allocates a reusable resource or group of resources on behalf of an actor without imposing any restrictions on the size or number of resources that can be allocated, in violation of the intended security policy for that actor.
CVE-2026-116686 is a Server-Side Request Forgery (SSRF) and Denial of Service (DoS) vulnerability in the official Model Context Protocol (MCP) Python SDK (mcp package on PyPI). When an MCP client validates structured tool execution results against a server-provided outputSchema, the SDK invokes the Python jsonschema library with its default URI resolution configuration. An untrusted or compromised MCP server can supply an outputSchema containing external $ref directives (using http://, https://, or file:// schemes), forcing the client process to synchronously fetch internal network resources, read local files, or block the main event loop indefinitely.
CVE-2026-116685 (GHSA-5h93-6whr-6q8j) is a sensitive information disclosure vulnerability in the official Model Context Protocol (MCP) Python SDK (`mcp` on PyPI). In versions 1.8.0 through 1.29.1 and 2.0.0 through 2.1.1, the SDK's HTTP-based client transports—including Streamable HTTP, Server-Sent Events (SSE), and OAuth client flows—unconditionally enable automatic HTTP redirect following (`follow_redirects=True`) across origin boundaries. When a configured endpoint returns a redirect to an external origin, the HTTP client forwards caller-supplied custom authentication headers (such as `X-API-Key`) and, in the case of HTTP 307 or 308 responses, replays the full POST request body containing JSON-RPC payloads or OAuth parameters to the untrusted destination.
CVE-2026-116684 is an incorrect authorization vulnerability (CWE-863) in the Model Context Protocol (MCP) Python SDK (`modelcontextprotocol/python-sdk`). The server-side bearer authentication middleware (`BearerAuthBackend`) verifies token cryptographic signatures and expiration timestamps via `TokenVerifier`, but fails to verify that the token's audience (`aud` claim or RFC 8707 resource indicator) matches the receiving MCP server's configured `resource_server_url`. In multi-service environments sharing a single OAuth 2.0 Authorization Server, an attacker possessing a valid token for an unrelated service can replay it against the MCP server to gain unauthorized access to protected tools, resources, and prompts.
CVE-2026-116675 is a stored Cross-Site Scripting (XSS) vulnerability in the TinyMCE rich text editor affecting versions 7.9.3 and 8.5.1 through 8.9.2. The flaw resides in the media plugin's serialization logic, where placeholder elements configured with the data-mce-object attribute are converted into real DOM elements using the active window's document context prior to sanitization. When an attacker supplies a media element with an invalid source attribute alongside an inline event handler attribute such as data-mce-p-onerror, the browser resolves the resource and executes the malicious JavaScript before the sanitizer can strip the dangerous attributes.
CVE-2026-116693 is a high-severity Regular Expression Denial of Service (ReDoS) vulnerability affecting the official Datadog Application Performance Monitoring (APM) client library for Node.js, dd-trace. The defect resides in the default regular expression defined by the DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP configuration parameter, which is used to sanitize incoming and outgoing HTTP URL query strings prior to recording trace span metadata. Due to overlapping sub-patterns and unanchored repetition blocks in the JSON Web Token (JWT) and URL-encoded string matching branches, the V8 JavaScript engine's regular expression subsystem (Irregexp) can be forced into catastrophic or high-degree polynomial backtracking. Because regular expression replacement occurs synchronously on the primary Node.js event loop, an unauthenticated remote attacker can send crafted HTTP requests with malicious query strings to consume 100% of the process CPU core, stalling concurrent request processing and rendering the application unavailable.
CVE-2026-116692 (GHSA-26p8-2jq9-3vq9) is a remote code execution vulnerability in MLflow affecting versions 3.12.0rc0 prior to 3.17.0. The flaw stems from an overly permissive module prefix check during the deserialization of third-party GenAI evaluation scorers in mlflow/genai/scorers/base.py. By setting an experiment's artifact root to a local package directory within the Python module search path and uploading a crafted Python file as a run artifact, an attacker can invoke the /ajax-api/3.0/mlflow/scorer/invoke endpoint to dynamically import and execute arbitrary code on the MLflow tracking server.