Skip to content

Commit 0d75f3c

Browse files
miloquinnsagzy
andauthored
Fixed malformed headers emitting junk trace IDs (#1897)
- Validate the W3C `traceparent` header before returning Cloud Logging trace context, so malformed or spoofed headers no longer emit junk trace IDs. - Parsing is delegated to `parseTraceParent` from `@opentelemetry/core` (already a direct dependency) rather than a hand-rolled parser. This enforces the spec rules — hex format, reserved version `ff`, all-zero trace/span IDs, no extra fields on version `00` — and correctly parses future-version headers that carry extra fields (the spec says receivers should read the first four fields). - `getTraceContext` is now a thin mapping: null-object shape on missing/invalid input, and `traceFlags & TraceFlags.SAMPLED` → `sampled`. - Add unit coverage: valid sampled/unsampled headers, a future-version forward-compat case, and a matrix of malformed headers (bad hex, wrong lengths, all-zero IDs, reserved version `ff`, wrong field count). --------- Co-authored-by: Sag <guptazy@gmail.com>
1 parent 39d5624 commit 0d75f3c

2 files changed

Lines changed: 78 additions & 5 deletions

File tree

src/helpers/context-header.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
1+
import { TraceFlags } from '@opentelemetry/api';
2+
import { parseTraceParent } from '@opentelemetry/core';
3+
14
export function getTraceContext(traceContext: string | undefined) {
25
if (!traceContext) {
36
return { traceId: null, spanId: null, sampled: null };
47
}
58

6-
const parts = traceContext.split('-');
9+
// Reject malformed/spoofed traceparent so we don't emit junk trace IDs
10+
// to Cloud Logging. parseTraceParent enforces the W3C Trace Context
11+
// rules (hex format, reserved version 'ff', all-zero trace/span IDs).
12+
const spanContext = parseTraceParent(traceContext);
713

8-
if (parts.length !== 4) {
14+
if (!spanContext) {
915
return { traceId: null, spanId: null, sampled: null };
1016
}
1117

12-
const [_version, traceId, spanId, flags] = parts;
13-
const sampled = (Number.parseInt(flags, 16) & 0x1) === 1;
14-
return { traceId, spanId, sampled };
18+
return {
19+
traceId: spanContext.traceId,
20+
spanId: spanContext.spanId,
21+
sampled: (spanContext.traceFlags & TraceFlags.SAMPLED) !== 0,
22+
};
1523
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { getTraceContext } from '@/helpers/context-header';
4+
5+
describe('getTraceContext', () => {
6+
const nullTraceContext = {
7+
traceId: null,
8+
spanId: null,
9+
sampled: null,
10+
};
11+
12+
it('returns null fields when traceparent is missing', () => {
13+
expect(getTraceContext(undefined)).toEqual(nullTraceContext);
14+
});
15+
16+
it('parses a valid sampled traceparent header', () => {
17+
expect(
18+
getTraceContext(
19+
'00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01',
20+
),
21+
).toEqual({
22+
traceId: '4bf92f3577b34da6a3ce929d0e0e4736',
23+
spanId: '00f067aa0ba902b7',
24+
sampled: true,
25+
});
26+
});
27+
28+
it('parses a valid unsampled traceparent header', () => {
29+
expect(
30+
getTraceContext(
31+
'00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00',
32+
),
33+
).toEqual({
34+
traceId: '4bf92f3577b34da6a3ce929d0e0e4736',
35+
spanId: '00f067aa0ba902b7',
36+
sampled: false,
37+
});
38+
});
39+
40+
it('parses a future-version traceparent header with extra fields', () => {
41+
expect(
42+
getTraceContext(
43+
'01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-extra',
44+
),
45+
).toEqual({
46+
traceId: '4bf92f3577b34da6a3ce929d0e0e4736',
47+
spanId: '00f067aa0ba902b7',
48+
sampled: true,
49+
});
50+
});
51+
52+
it.each([
53+
'00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7',
54+
'00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-extra',
55+
'ff-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01',
56+
'zz-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01',
57+
'00-xyz-00f067aa0ba902b7-01',
58+
'00-00000000000000000000000000000000-00f067aa0ba902b7-01',
59+
'00-4bf92f3577b34da6a3ce929d0e0e4736-xyz-01',
60+
'00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01',
61+
'00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-zz',
62+
])('returns null fields for invalid traceparent %s', (traceparent) => {
63+
expect(getTraceContext(traceparent)).toEqual(nullTraceContext);
64+
});
65+
});

0 commit comments

Comments
 (0)