Skip to content

Commit 3d49de5

Browse files
mabdinurbrettlangdonmajorgreys
authored
fix(w3c): fix traceparent validation (backport #4791 to 1.7) (#4803)
## Description Running the W3C trace context parametric tests (introduced [here](DataDog/system-tests#691)) surfaced four issues with how the python tracer propagates traceparent and tracestate headers. These issues are described below and are fixed in this PR. ### Edge Case 1 (most important) Currently `trace_parent.islower()` is used to check if a traceparent contains lower case letters and digits (uppercase letters are invalid). Unfortunately `trace_parent.islower()` also evaluates to False if there are no lowercase letters are present in the traceparent (ex: traceparent=00-12312312003-1232131321 would be marked as invalid and dropped). This is incorrect. Fix: ddb9dc5. Since the traceparent should contain 48 randomly generated hex values it is unlikely to only contain digits. This edge case is very unlikely to occur. Current behavior: - `00-123123123-23123213AAA` -> uppercase letter detected traceparent is ignored - `00-123123123-23123213132` -> no lowercase letter detected traceparent is ignored [WRONG] - `00-123123123-2312322aaaa` -> lowercase letter detected traceparent is propagated Expected behavior: - `00-123123123-23123213132AAAA` -> uppercase detected traceparent is ignored - `00-123123123-23123213132` -> no uppercase detected traceparent is propagated - `00-123123123-23123213132aaaa` -> no uppercase detected traceparent is propagated ### Edge Case 2 If traceparent contains an invalid characters (ex: period) do not propagate the traceparent. Start a new trace. Currently we can propagate invalid trace_ids (this is because we truncate the trace_id and only convert the last 16 digits to hex, we ignore all the characters in the first 16 digits). Fix: ae1e73e ### Edge Case 3 Ensure that traceflags contain ONLY two digits (`00` or `01`). `000` and `001` are examples of invalid values that should not be propagated. Fix: adc4f80 ### Edge Case 4 Ensure traceparent version contain ONLY two digits (`00` or `01`). `000` and `001` are examples of invalid values that should not be propagated. Fix: 2e416c5 ### Edge Case 5 The W3C specification is additive. Although only traceparents with the version `00` are supported we should attempt to parse traceparent with different formats. Example: "01-12345678901234567890123456789012-1234567890123456-01-what-the-future-will-be-like" In the traceparent example above we should parse the version, trace id, span id, and sample flag and ignore the trailing values. Fix: 4cebe7514682787f6068754037fba569f4af3d60 ### Edge Case 6 This edge case is not addressed in this PR. I am including it here for completeness. In the W3C tracecontext specification a tracer SHOULD set two http headers, one header should set the tracestate and the other should set the traceparent. However, if duplicate traceparent and tracestate headers are received, the tracer must processes and reconcile these headers (logic: 1) receive duplicate traceparent headers with different values -> drop these values and start a new trace. 2) receive duplicate tracestates with different tags -> combine the tracestates and propagate it). In the ddtrace library http headers are added to a dictionary, so duplicate http header values are overwritten. To address this edge case the tracer must be able to store detect and store all key value pairs in http headers. Since this edge case requires significant changes to distributed tracing and does not resolve a critical issue this work can be deferred. ## Testing Testing will covered by system tests. ## Reviewer Checklist - [ ] Title is accurate. - [ ] Description motivates each change. - [ ] No unnecessary changes were introduced in this PR. - [ ] Avoid breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes unless absolutely necessary. - [ ] Tests provided or description of manual testing performed is included in the code or PR. - [ ] Release note has been added and follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/contributing.html#Release-Note-Guidelines), or else `changelog/no-changelog` label added. - [ ] All relevant GitHub issues are correctly linked. - [ ] Backports are identified and tagged with Mergifyio. Co-authored-by: Brett Langdon <[email protected]> Co-authored-by: Tahir H. Butt <[email protected]> Co-authored-by: Brett Langdon <[email protected]> Co-authored-by: Tahir H. Butt <[email protected]>
1 parent cc4bac9 commit 3d49de5

File tree

2 files changed

+80
-40
lines changed

2 files changed

+80
-40
lines changed

ddtrace/propagation/http.py

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,23 @@ def _possible_header(header):
7373
_POSSIBLE_HTTP_HEADER_TRACESTATE = _possible_header(_HTTP_HEADER_TRACESTATE)
7474

7575

76+
# https://www.w3.org/TR/trace-context/#traceparent-header-field-values
77+
# Future proofing: The traceparent spec is additive, future traceparent versions may contain more than 4 values
78+
# The regex below matches the version, trace id, span id, sample flag, and end-string/future values (if version>00)
79+
_TRACEPARENT_HEX_REGEX = re.compile(
80+
r"""
81+
^ # Start of string
82+
([a-f0-9]{2})- # 2 character hex version
83+
([a-f0-9]{32})- # 32 character hex trace id
84+
([a-f0-9]{16})- # 16 character hex span id
85+
([a-f0-9]{2}) # 2 character hex sample flag
86+
(-.+)? # optional, start of any additional values
87+
$ # end of string
88+
""",
89+
re.VERBOSE,
90+
)
91+
92+
7693
def _extract_header_value(possible_header_names, headers, default=None):
7794
# type: (FrozenSet[str], Dict[str, str], Optional[str]) -> Optional[str]
7895
for header in possible_header_names:
@@ -550,35 +567,41 @@ def _get_traceparent_values(tp):
550567
Otherwise we extract the trace-id, span-id, and sampling priority from the
551568
traceparent header.
552569
"""
570+
valid_tp_values = _TRACEPARENT_HEX_REGEX.match(tp.strip())
571+
if valid_tp_values is None:
572+
raise ValueError("Invalid traceparent version: %s" % tp)
573+
574+
(
575+
version,
576+
trace_id_hex,
577+
span_id_hex,
578+
trace_flags_hex,
579+
future_vals,
580+
) = valid_tp_values.groups() # type: Tuple[str, str, str, str, Optional[str]]
553581

554-
version, trace_id_hex, span_id_hex, trace_flags_hex = tp.strip().split("-")
555-
# check version is a valid hexadecimal, if not it's invalid we will move on to the next prop method
556-
int(version, 16)
557-
# https://www.w3.org/TR/trace-context/#version
558582
if version == "ff":
559-
raise ValueError("'ff' is an invalid traceparent version")
560-
# currently 00 is the only version format, but if future versions come up we may need to add changes
561-
if version != "00":
583+
# https://www.w3.org/TR/trace-context/#version
584+
raise ValueError("ff is an invalid traceparent version: %s" % tp)
585+
elif version != "00":
586+
# currently 00 is the only version format, but if future versions come up we may need to add changes
562587
log.warning("unsupported traceparent version:%r, still attempting to parse", version)
588+
elif version == "00" and future_vals is not None:
589+
raise ValueError("Traceparents with the version `00` should contain 4 values delimited by a dash: %s" % tp)
563590

564-
if len(trace_id_hex) == 32 and len(span_id_hex) == 16 and len(trace_flags_hex) >= 2:
565-
trace_id = _hex_id_to_dd_id(trace_id_hex)
566-
span_id = _hex_id_to_dd_id(span_id_hex)
591+
trace_id = _hex_id_to_dd_id(trace_id_hex)
592+
span_id = _hex_id_to_dd_id(span_id_hex)
567593

568-
# All 0s are invalid values
569-
if trace_id == 0:
570-
raise ValueError("0 value for trace_id is invalid")
571-
if span_id == 0:
572-
raise ValueError("0 value for span_id is invalid")
594+
# All 0s are invalid values
595+
if trace_id == 0:
596+
raise ValueError("0 value for trace_id is invalid")
597+
if span_id == 0:
598+
raise ValueError("0 value for span_id is invalid")
573599

574-
trace_flags = _hex_id_to_dd_id(trace_flags_hex)
575-
# there's currently only one trace flag, which denotes sampling priority
576-
# was set to keep "01" or drop "00"
577-
# trace flags is a bit field: https://www.w3.org/TR/trace-context/#trace-flags
578-
sampling_priority = trace_flags & 0x1
579-
580-
else:
581-
raise ValueError("W3C traceparent hex length incorrect: %s" % tp)
600+
trace_flags = _hex_id_to_dd_id(trace_flags_hex)
601+
# there's currently only one trace flag, which denotes sampling priority
602+
# was set to keep "01" or drop "00"
603+
# trace flags is a bit field: https://www.w3.org/TR/trace-context/#trace-flags
604+
sampling_priority = trace_flags & 0x1
582605

583606
return trace_id, span_id, sampling_priority
584607

@@ -644,10 +667,6 @@ def _extract(headers):
644667
if tp is None:
645668
log.debug("no traceparent header")
646669
return None
647-
# uppercase char in tp makes it invalid:
648-
# https://www.w3.org/TR/trace-context/#traceparent-header-field-values
649-
if not tp.islower():
650-
raise ValueError("uppercase characters are not allowed in traceparent")
651670
trace_id, span_id, sampling_priority = _TraceContext._get_traceparent_values(tp)
652671
except (ValueError, AssertionError):
653672
log.exception("received invalid w3c traceparent: %s ", tp)

tests/tracer/test_propagation.py

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -444,34 +444,52 @@ def test_tracecontext_get_sampling_priority(sampling_priority_tp, sampling_prior
444444
None,
445445
),
446446
(
447-
"01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
447+
"01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-what-the-future-looks-like",
448448
# tp, trace_id, span_id, sampling_priority
449449
(11803532876627986230, 67667974448284343, 1),
450450
["unsupported traceparent version:'01', still attempting to parse"],
451451
None,
452452
),
453453
(
454-
"0-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
454+
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-v00-can-not-have-future-values",
455455
# tp, trace_id, span_id, sampling_priority
456456
(11803532876627986230, 67667974448284343, 1),
457-
["unsupported traceparent version:'0', still attempting to parse"],
457+
[],
458+
ValueError,
459+
),
460+
(
461+
"0-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
462+
# tp, trace_id, span_id, sampling_priority
463+
None,
464+
[],
465+
ValueError,
466+
),
467+
(
468+
"ff-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
469+
# tp, trace_id, span_id, sampling_priority
458470
None,
471+
[],
472+
ValueError,
473+
),
474+
(
475+
"00-4BF92K3577B34dA6C3ce929d0e0e4736-00f067aa0ba902b7-01",
476+
# tp, trace_id, span_id, sampling_priority
477+
None,
478+
[],
479+
ValueError,
459480
),
460481
(
461482
"00-f92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
462483
# tp, trace_id, span_id, sampling_priority
463484
None,
464-
[
465-
"received invalid w3c traceparent: 00-f92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01.",
466-
"W3C traceparent hex length incorrect: 00-f92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
467-
],
485+
[],
468486
ValueError,
469487
),
470488
( # we still parse the trace flag and analyze the it as a bit field
471489
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-02",
472490
# tp, trace_id, span_id, sampling_priority
473491
(11803532876627986230, 67667974448284343, 0),
474-
None,
492+
[],
475493
None,
476494
),
477495
],
@@ -481,7 +499,10 @@ def test_tracecontext_get_sampling_priority(sampling_priority_tp, sampling_prior
481499
"invalid_0_value_for_span_id",
482500
"traceflag_00",
483501
"unsupported_version",
502+
"version_00_with_unsupported_trailing_values",
484503
"short_version",
504+
"invalid_version",
505+
"traceparent_contains_uppercase_chars",
485506
"short_trace_id",
486507
"unknown_trace_flag",
487508
],
@@ -490,14 +511,14 @@ def test_extract_traceparent(caplog, headers, expected_tuple, expected_logging,
490511
with caplog.at_level(logging.DEBUG):
491512
if expected_exception:
492513
with pytest.raises(expected_exception):
493-
traceparent_values = _TraceContext._get_traceparent_values(headers)
494-
assert traceparent_values == expected_tuple
514+
_TraceContext._get_traceparent_values(headers)
495515
else:
496516
traceparent_values = _TraceContext._get_traceparent_values(headers)
497517
assert traceparent_values == expected_tuple
498-
if caplog.text or expected_logging:
499-
for expected_log in expected_logging:
500-
assert expected_log in caplog.text
518+
519+
if caplog.text or expected_logging:
520+
for expected_log in expected_logging:
521+
assert expected_log in caplog.text
501522

502523

503524
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)