From 04545c1c18619a6237d15432ae133936c24af95c Mon Sep 17 00:00:00 2001 From: Milo Date: Wed, 17 Jun 2026 08:23:14 +0800 Subject: [PATCH] fix: validate traceparent before using trace context --- src/helpers/context-header.ts | 15 ++++++- src/helpers/context-header.unit.test.ts | 52 +++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 src/helpers/context-header.unit.test.ts diff --git a/src/helpers/context-header.ts b/src/helpers/context-header.ts index 40667fdc..b3154216 100644 --- a/src/helpers/context-header.ts +++ b/src/helpers/context-header.ts @@ -9,7 +9,20 @@ export function getTraceContext(traceContext: string | undefined) { return { traceId: null, spanId: null, sampled: null }; } - const [_version, traceId, spanId, flags] = parts; + const [version, traceId, spanId, flags] = parts; + + const isValidTraceContext = + /^[0-9a-f]{2}$/.test(version) && + /^[0-9a-f]{32}$/.test(traceId) && + traceId !== '00000000000000000000000000000000' && + /^[0-9a-f]{16}$/.test(spanId) && + spanId !== '0000000000000000' && + /^[0-9a-f]{2}$/.test(flags); + + if (!isValidTraceContext) { + return { traceId: null, spanId: null, sampled: null }; + } + const sampled = (Number.parseInt(flags, 16) & 0x1) === 1; return { traceId, spanId, sampled }; } diff --git a/src/helpers/context-header.unit.test.ts b/src/helpers/context-header.unit.test.ts new file mode 100644 index 00000000..d25738a1 --- /dev/null +++ b/src/helpers/context-header.unit.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from 'vitest'; + +import { getTraceContext } from '@/helpers/context-header'; + +describe('getTraceContext', () => { + const nullTraceContext = { + traceId: null, + spanId: null, + sampled: null, + }; + + it('returns null fields when traceparent is missing', () => { + expect(getTraceContext(undefined)).toEqual(nullTraceContext); + }); + + it('parses a valid sampled traceparent header', () => { + expect( + getTraceContext( + '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01', + ), + ).toEqual({ + traceId: '4bf92f3577b34da6a3ce929d0e0e4736', + spanId: '00f067aa0ba902b7', + sampled: true, + }); + }); + + it('parses a valid unsampled traceparent header', () => { + expect( + getTraceContext( + '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00', + ), + ).toEqual({ + traceId: '4bf92f3577b34da6a3ce929d0e0e4736', + spanId: '00f067aa0ba902b7', + sampled: false, + }); + }); + + it.each([ + '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7', + '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-extra', + 'zz-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01', + '00-xyz-00f067aa0ba902b7-01', + '00-00000000000000000000000000000000-00f067aa0ba902b7-01', + '00-4bf92f3577b34da6a3ce929d0e0e4736-xyz-01', + '00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01', + '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-zz', + ])('returns null fields for invalid traceparent %s', (traceparent) => { + expect(getTraceContext(traceparent)).toEqual(nullTraceContext); + }); +});