Skip to content

Commit 350d322

Browse files
authored
fix(diagnostics): harden trace context parsing (#70955)
1 parent bae7b54 commit 350d322

2 files changed

Lines changed: 53 additions & 5 deletions

File tree

src/infra/diagnostic-trace-context.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,27 @@ describe("diagnostic-trace-context", () => {
4545

4646
it("rejects malformed traceparent values", () => {
4747
expect(parseDiagnosticTraceparent(undefined)).toBeUndefined();
48+
expect(parseDiagnosticTraceparent(`00-${TRACE_ID}-${SPAN_ID}-01-extra`)).toBeUndefined();
4849
expect(parseDiagnosticTraceparent(`ff-${TRACE_ID}-${SPAN_ID}-01`)).toBeUndefined();
4950
expect(parseDiagnosticTraceparent(`00-${"0".repeat(32)}-${SPAN_ID}-01`)).toBeUndefined();
5051
expect(parseDiagnosticTraceparent(`00-${TRACE_ID}-${"0".repeat(16)}-01`)).toBeUndefined();
5152
expect(parseDiagnosticTraceparent(`00-${TRACE_ID}-${SPAN_ID}-xyz`)).toBeUndefined();
5253
});
5354

55+
it("rejects oversized traceparent values before parsing", () => {
56+
expect(
57+
parseDiagnosticTraceparent(`00-${TRACE_ID}-${SPAN_ID}-01-${"a".repeat(128)}`),
58+
).toBeUndefined();
59+
});
60+
61+
it("continues future-version traceparents from the first four fields", () => {
62+
expect(parseDiagnosticTraceparent(`01-${TRACE_ID}-${SPAN_ID}-01-extra`)).toEqual({
63+
traceId: TRACE_ID,
64+
spanId: SPAN_ID,
65+
traceFlags: "01",
66+
});
67+
});
68+
5469
it("creates a normalized context from explicit fields or traceparent", () => {
5570
expect(
5671
createDiagnosticTraceContext({
@@ -71,6 +86,14 @@ describe("diagnostic-trace-context", () => {
7186
});
7287
});
7388

89+
it("generates valid non-zero ids for fallback contexts", () => {
90+
const context = createDiagnosticTraceContext();
91+
92+
expect(isValidDiagnosticTraceId(context.traceId)).toBe(true);
93+
expect(isValidDiagnosticSpanId(context.spanId)).toBe(true);
94+
expect(formatDiagnosticTraceparent(context)).toBeDefined();
95+
});
96+
7497
it("creates child contexts without retaining parent references or self-parenting", () => {
7598
const parent = createDiagnosticTraceContext({
7699
traceId: TRACE_ID,

src/infra/diagnostic-trace-context.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import { randomBytes } from "node:crypto";
22

33
const TRACEPARENT_VERSION = "00";
44
const DEFAULT_TRACE_FLAGS = "01";
5+
const MAX_TRACEPARENT_LENGTH = 128;
56
const TRACE_ID_RE = /^[0-9a-f]{32}$/;
67
const SPAN_ID_RE = /^[0-9a-f]{16}$/;
78
const TRACE_FLAGS_RE = /^[0-9a-f]{2}$/;
9+
const TRACEPARENT_VERSION_RE = /^[0-9a-f]{2}$/;
810

911
export type DiagnosticTraceContext = {
1012
/** W3C trace id, 32 lowercase hex chars. */
@@ -29,6 +31,22 @@ function isNonZeroHex(value: string): boolean {
2931
return !/^0+$/.test(value);
3032
}
3133

34+
function randomTraceId(): string {
35+
let traceId = randomHex(16);
36+
while (!isNonZeroHex(traceId)) {
37+
traceId = randomHex(16);
38+
}
39+
return traceId;
40+
}
41+
42+
function randomSpanId(): string {
43+
let spanId = randomHex(8);
44+
while (!isNonZeroHex(spanId)) {
45+
spanId = randomHex(8);
46+
}
47+
return spanId;
48+
}
49+
3250
export function isValidDiagnosticTraceId(value: unknown): value is string {
3351
return typeof value === "string" && TRACE_ID_RE.test(value) && isNonZeroHex(value);
3452
}
@@ -68,12 +86,19 @@ function normalizeTraceFlags(value: unknown): string | undefined {
6886
export function parseDiagnosticTraceparent(
6987
traceparent: string | undefined,
7088
): DiagnosticTraceContext | undefined {
71-
const parts = traceparent?.trim().toLowerCase().split("-");
72-
if (!parts || parts.length !== 4) {
89+
if (typeof traceparent !== "string" || traceparent.length > MAX_TRACEPARENT_LENGTH) {
90+
return undefined;
91+
}
92+
const parts = traceparent.trim().toLowerCase().split("-");
93+
if (!parts || parts.length < 4) {
7394
return undefined;
7495
}
7596
const [version, traceId, spanId, traceFlags] = parts;
76-
if (version !== TRACEPARENT_VERSION) {
97+
if (
98+
!TRACEPARENT_VERSION_RE.test(version) ||
99+
version === "ff" ||
100+
(version === TRACEPARENT_VERSION && parts.length !== 4)
101+
) {
77102
return undefined;
78103
}
79104
const normalizedTraceId = normalizeTraceId(traceId);
@@ -108,8 +133,8 @@ export function createDiagnosticTraceContext(
108133
input: DiagnosticTraceContextInput = {},
109134
): DiagnosticTraceContext {
110135
const parsed = parseDiagnosticTraceparent(input.traceparent);
111-
const traceId = normalizeTraceId(input.traceId) ?? parsed?.traceId ?? randomHex(16);
112-
const spanId = normalizeSpanId(input.spanId) ?? parsed?.spanId ?? randomHex(8);
136+
const traceId = normalizeTraceId(input.traceId) ?? parsed?.traceId ?? randomTraceId();
137+
const spanId = normalizeSpanId(input.spanId) ?? parsed?.spanId ?? randomSpanId();
113138
const parentSpanId = normalizeSpanId(input.parentSpanId);
114139
return {
115140
traceId,

0 commit comments

Comments
 (0)