Skip to content

feat: add inbound header validation and tests to trace package - #3

Open
daveseddon-runpod wants to merge 4 commits into
masterfrom
feat/trace-validation-and-tests
Open

daveseddon-runpod wants to merge 4 commits into
masterfrom
feat/trace-validation-and-tests

Conversation

@daveseddon-runpod

Copy link
Copy Markdown

What

Harden and test the shared trace package — the cross-service X-Trace-* header contract that host, hapi, proxy, and the RunPod GraphQL API all speak.

Part of the end-to-end X-Trace-ID tracing effort (RunPod GraphQL → hapi/proxy → host, propagate-or-generate at every hop).

Changes

Go — trace/trace.go

  • Inbound-header validation: an X-Trace-ID / X-Request-ID that is empty, over maxIDLen (200), or contains a byte outside [A-Za-z0-9._-] (CR/LF, control, space) is rejected and regenerated — prevents header/log injection (CWE-93). X-Trace-Source / X-Request-Source are validated and blanked (not regenerated) when invalid.
  • Allocation-free byte-scan validator (validID) — 0 allocs/op.
  • Guarded the X-Trace-Start parse so the common no-header path skips time.Parse.
  • Removed the now-unused orelse generic.

Python — py/trace.py

  • Fixed Trace.new() (datetime.now() AttributeError) and the save_to_headers tuple bug (X-Request-ID stored as a 1-tuple via a trailing comma); now emits the same five headers as Go with a fresh X-Request-ID.
  • Mirrored the Go validation (_valid_id / _resolve_id / _resolve_source).

Tests

  • trace/trace_test.go — table-driven ({description, input, expected}): validID, resolveID, resolveSource, FromHeaderOrNew (present/absent/poisoned/source-drop), trace-start (absent/past/invalid/future-clamp), SaveToHeader round-trip, ClientMiddleware/ServerMiddleware, New() uniqueness (10k), AllocsPerRun (0-alloc validID), plus benchmarks.
  • py/trace_test.pyunittest table-driven mirror incl. a regression test per fixed bug.

Docs — README ## Tracing now carries the header contract table, Go inbound/outbound/response-echo usage, and the inbound-validation behavior.

Benchmarks (-benchmem)

BenchmarkFromHeaderOrNew/present     1057 ns/op    32 B/op    2 allocs/op
BenchmarkFromHeaderOrNew/absent      1899 ns/op   160 B/op    6 allocs/op
BenchmarkFromHeaderOrNew/invalid     2220 ns/op   160 B/op    6 allocs/op
BenchmarkValidID                      128 ns/op     0 B/op    0 allocs/op
BenchmarkSaveToHeader                1694 ns/op   136 B/op    8 allocs/op
BenchmarkClientMiddlewareRoundTrip   3982 ns/op   872 B/op   15 allocs/op

validID meets the 0-alloc requirement; the 6-alloc paths are unavoidable UUID generation. Negligible against network I/O.

Verification

  • Go: go test -race ./... pass; benchmarks run.
  • Python: unittest — 8 tests pass.

Follow-up

Tag a new module version (e.g. v0.1.2) so host / hapi / proxy can bump their require and pick up the inbound sanitization. Backward compatible — the only visible change is that a hostile inbound id is regenerated.

🤖 Generated with Claude Code

randomizedcoder and others added 3 commits September 21, 2026 17:05
Validate inbound X-Trace-ID/X-Request-ID (length + charset, regenerate on failure) to prevent header/log injection; fix Python Trace.new() and save_to_headers tuple bug and bring it to five-header parity with Go; add table-driven Go + Python tests and benchmarks. Part of the end-to-end X-Trace-ID tracing effort.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… lint

- py: parse/validate inbound X-Trace-Start (RFC3339, fall back to now) and drop the non-contract X-Request-Start read, closing a CWE-93 header-injection vector where a poisoned timestamp was re-emitted verbatim by save_to_headers
- py: _resolve_source drops empty/invalid to "" to match Go's resolveSource; datetime.now(timezone.utc) for correct UTC; new() return annotation; Optional -> | None
- py tests: add poisoned/invalid X-Trace-Start rows + valid-preserved row
- go: extract X-Trace-* header names to consts (SaveToHeader/FromHeaderOrNew); close response bodies in trace tests; reuse fixture consts; fix an empty-source test description
- docs: clarify request_id is minted fresh per outbound sub-request; the inbound edge honors a valid caller-supplied X-Request-ID

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@daveseddon-runpod
daveseddon-runpod marked this pull request as ready for review September 22, 2026 02:07
FromHeaderOrNew logged an slog.Warn once per request whenever an inbound
X-Trace-Start was in the future. That value is caller-controlled, so an
untrusted client could drive a consuming service's log volume by sending
a future timestamp on every request. A malformed start already falls
back to now with no log; the future case is now handled the same way,
folded into the parse guard.

Align the Python resolver, which previously neither clamped nor warned
on a future start (it propagated it verbatim): it now clamps to now to
match Go, covered by a regression test.

This removes the need for each public-facing consumer to strip inbound
X-Trace-Start defensively.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants