fix(ui): encode trace/span ids in cross-signal deep-links (JEF-534)#143
Merged
thejefflarson merged 1 commit intoJul 27, 2026
Conversation
trace_id/span_id are ingested OTLP data — attacker-shapeable — but several components interpolated them raw into react-router `to=`/`navigate()` targets while the neighboring `service` was already `encodeURIComponent`-ed (TraceWaterfall's span-detail "logs for this span" link and its header "logs" link, LogView's trace/span drill-in links, MetricChart's exemplar click, and TraceList's row link via App.tsx). react-router keeps these same-origin/client-side, so no XSS and no open-redirect — worst case an id containing `?`/`#`/`&` lands on an unexpected in-app route or injects a stray query param. Pure hardening for consistency, not an active exploit. Extracted the fix into one shared `ui/src/links.ts`: `traceHref` builds `/traces/:id` (path segment `encodeURIComponent`-ed, optional span/service via URLSearchParams) and `logsHref` builds `/logs` the same way. All five call sites now import from there instead of each re-deriving its own raw-interpolated string, which also collapses three near-identical `/traces/:id?service=` builders the first pass of this fix had introduced per-file. Test: ui/src/links.test.ts pins the well-formed-hex-id shape byte-identical to the pre-fix output (no behavior change for the common case) and locks that a `&`/`#` in an id is encoded rather than landing as a stray param or route. `npm run build` (tsc + vite) and `npx vitest run` both clean (6 test files, 33 tests); `npm run lint` clean. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
thejefflarson
deleted the
thejefflarson/jef-534-encode-tracespan-ids-in-ui-links-with-encodeuricomponent
branch
July 27, 2026 01:57
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes JEF-534.
Problem
trace_id/span_idcome from ingested OTLP data — attacker-shapeable — butseveral UI components interpolated them raw into react-router
to=/navigate()targets, while the neighboringservicewas consistentlyencodeURIComponent-wrapped:TraceWaterfall.tsx— the span-detail "logs for this span" link(
trace_id+span_id) and the header "logs" link (trace_id).LogView.tsx— the trace/span drill-in links (traceLink,spanInTraceLink) and the trace-cell link.MetricChart.tsx— the exemplar-click navigation target.TraceList's row link, wired up inApp.tsx.react-router keeps these same-origin/client-side, so no XSS, no
open-redirect — worst case an id containing
?/#/&lands on anunexpected in-app route or injects a stray query param. Pure hardening /
consistency, not an active exploit.
Fix
Extracted the fix into one shared
ui/src/links.ts:traceHref(traceId, { spanId?, service? })builds/traces/:id(
encodeURIComponent-ed path segment, optional span/service viaURLSearchParams).logsHref(traceId, spanId?)builds/logsthe same way.All five call sites now import from there instead of each re-deriving its
own raw-interpolated string — this also collapsed three near-identical
/traces/:id?service=builders the first pass of this fix had introducedper-file (caught in the
/simplifypass, see below).Sites fixed (grep-confirmed against
rg -n "trace_id=\$\{|span_id=\$\{|/traces/\$\{|/logs\?" ui/srcplus a manual sweep forto(t.trace_id)-style indirection that grep missed):ui/src/components/TraceWaterfall.tsx(2 sites)ui/src/components/LogView.tsx(3 sites)ui/src/components/MetricChart.tsx(1 site)ui/src/App.tsx(1 site —TraceList's row link, found by tracing thetoprop, not by the literal grep pattern)Testing
ui/src/links.test.tspins the well-formed-hex-id shape byte-identical tothe pre-fix output (no behavior change for the common case) and locks
that a
&/#in an id isencodeURIComponent-encoded rather than landingas a stray query param or truncating the route.
npm run build(tsc --noEmit && vite build) — cleannpx vitest run— clean, 6 test files / 33 testsnpm run lint(eslint) — cleanDecisions
ui/src/api.ts'sgetTrace(traceId)builds a backend fetchURL (
/api/traces/${traceId}), not a react-router link — different threatmodel (a malformed id there just 404s against the actual HTTP request, not
a client-side route). Left out of scope per the ticket's framing ("UI
links"); flagging in case a follow-up wants the same treatment for
API-fetch paths.
this diff) into one
ui/src/links.tsmodule during the/simplifypass —reduces the diff's own footprint rather than leaving four copies of the
same logic.
🤖 Generated with Claude Code
https://claude.ai/code/session_01JbrmfzHsTMMzPaSrUkZgWo