Skip to content

security: validate client-supplied trace IDs#26

Merged
ankurs merged 5 commits intomainfrom
fix/validate-trace-ids
Apr 4, 2026
Merged

security: validate client-supplied trace IDs#26
ankurs merged 5 commits intomainfrom
fix/validate-trace-ids

Conversation

@ankurs
Copy link
Copy Markdown
Member

@ankurs ankurs commented Apr 4, 2026

Summary

  • Add validateTraceID helper: max 128 chars, printable ASCII only (0x20-0x7E), strips control/unicode bytes
  • Called in SetTraceIdWithValue (after gRPC metadata extraction) and UpdateTraceId (before storing)
  • If all characters are invalid, falls back to generating a new UUID
  • Protects structured logs, Sentry/Rollbar, and OTEL span attributes from log injection and DoS

Test plan

  • TestValidateTraceID — table-driven: normal, truncation, stripping, edge cases
  • TestSetTraceId_ValidatesMetadataTraceID — dirty metadata sanitized
  • TestSetTraceId_TruncatesLongMetadataTraceID — 200 chars → 128
  • TestUpdateTraceId_ValidatesInput — dirty input sanitized
  • TestUpdateTraceId_AllInvalidFallsBackToGenerated — generates new UUID
  • make test, make lint, make doc all pass

Summary by CodeRabbit

  • New Features

    • Added ability to obtain the updated context along with the resolved trace ID.
    • Added a configurable trace-ID validator hook so callers can supply custom validation or disable it.
  • Improvements

    • Trace IDs are automatically sanitized (printable ASCII only, truncated to 128 chars).
    • Reduced default async-notification limit from 1000 to 20 and clarified concurrency semantics.
  • Documentation

    • Updated README links and API docs to reflect these changes.
  • Tests

    • Added unit tests covering trace-ID sanitization and validator customization.

Add validateTraceID helper that sanitizes trace IDs from gRPC metadata
and proto fields: max 128 chars, printable ASCII only (0x20-0x7E).
Non-printable/control/unicode bytes are stripped. If all characters are
invalid, falls back to generating a new UUID.

Called in SetTraceIdWithValue (after metadata extraction) and UpdateTraceId
(before storing). Protects logs, Sentry/Rollbar, and OTEL span attributes
from injection attacks and DoS via oversized trace IDs.
Copilot AI review requested due to automatic review settings April 4, 2026 06:20
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

Warning

Rate limit exceeded

@ankurs has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 12 minutes and 20 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 12 minutes and 20 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4cfbf688-149a-403c-bd5e-e81f4e03fb81

📥 Commits

Reviewing files that changed from the base of the PR and between d90ab7a and 2820c4a.

📒 Files selected for processing (2)
  • notifier/README.md
  • notifier/notifier.go
📝 Walkthrough

Walkthrough

Trace ID sanitization and configurable validation were added to the notifier package: trace IDs are normalized (printable ASCII only, max 128 chars) and sanitized before use; a package-level validator can be overridden or disabled. README docs were updated and unit tests added to cover the behavior.

Changes

Cohort / File(s) Summary
Top-level README
README.md
Updated GitHub source anchor line numbers for multiple exported function links.
Notifier docs
notifier/README.md
Added docs for SetTraceIDValidator and SetTraceIdWithValue; changed SetMaxAsyncNotifications default to 20 and clarified concurrency semantics; adjusted many source anchor links.
Notifier logic
notifier/notifier.go
Added validateTraceID sanitizer (printable ASCII 0x20–0x7E, truncate to 128 chars), introduced package-level traceIDValidator and exported SetTraceIDValidator(fn func(string) string), applied sanitization in SetTraceIdWithValue and UpdateTraceId.
Tests
notifier/notifier_test.go
Added unit tests covering sanitization behavior, truncation, removal of control/non-ASCII chars, fallback generation, and custom/nil validator behavior.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Client
participant Notifier
participant OTEL
participant Logs
Client->>Notifier: incoming gRPC metadata (trace-id)
Notifier->>Notifier: extract raw trace-id
Notifier->>Notifier: traceIDValidator(raw) → sanitizedID
alt sanitizedID empty
Notifier->>OTEL: try OTEL span trace-id
OTEL-->>Notifier: otelTraceID or empty
alt otelTraceID empty
Notifier->>Notifier: generate UUID trace-id
Notifier->>Logs: attach generated trace-id
Notifier->>OTEL: set span attribute (generated)
else
Notifier->>Logs: attach otelTraceID
Notifier->>OTEL: set span attribute (otelTraceID)
end
else
Notifier->>Logs: attach sanitizedID
Notifier->>OTEL: set span attribute (sanitizedID)
end

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hop through metadata bright,
Nibbling bad bytes out of sight,
Truncate to one-twenty-eight,
Keep printable chars — oh great!
Now traces hop clean, snug, and light ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'security: validate client-supplied trace IDs' directly and accurately summarizes the main change: adding validation for client-supplied trace IDs to prevent log injection and DoS attacks.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/validate-trace-ids

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens trace ID handling in the notifier package by sanitizing client-supplied trace IDs before they’re propagated into structured logs, error reporters (Sentry/Rollbar/Airbrake), and OTEL span attributes.

Changes:

  • Added validateTraceID helper (printable ASCII only, max 128 chars) and applied it to metadata-derived IDs in SetTraceIdWithValue and to caller-supplied IDs in UpdateTraceId.
  • Added notifier unit tests covering sanitization, truncation, and fallback behavior.
  • Regenerated README docs (function index/anchors) to reflect current code.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
README.md Updates generated doc links/anchors after code movement.
notifier/README.md Updates generated notifier docs, including new/updated trace ID APIs and line anchors.
notifier/notifier.go Introduces validateTraceID and applies sanitization to trace ID resolution/update paths.
notifier/notifier_test.go Adds table-driven tests for trace ID sanitization and integration tests for metadata/update flows.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
notifier/notifier_test.go (1)

242-253: Add an explicit unicode-stripping test case.

You validate control-byte stripping, but there’s no direct unicode input assertion yet.

➕ Suggested test case
 	{
 		{"spaces preserved", "abc def", "abc def"},
 		{"printable special chars preserved", "abc!@#$%^&*()def", "abc!@#$%^&*()def"},
+		{"unicode stripped", "abcé漢def", "abcdef"},
 		{"only non-printable returns empty", "\x00\x01\x02", ""},
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@notifier/notifier_test.go` around lines 242 - 253, Add a new table test row
to explicitly assert how multibyte Unicode is handled: insert an entry alongside
the existing cases (e.g., next to "printable special chars preserved") such as
{"unicode preserved", "héllø—你好", "héllø—你好"} to verify the sanitizer preserves
non-ASCII printable Unicode characters; add or adjust expected output according
to the behavior of the sanitizer function used in these tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@notifier/notifier.go`:
- Around line 637-640: UpdateTraceId currently only checks traceID == "" after
validateTraceID, which lets whitespace-only IDs be stored; update it to use the
same "empty after trimming" fallback as SetTraceIdWithValue by trimming the
validated traceID (e.g., strings.TrimSpace(traceID)) and if the result is empty
call SetTraceId(ctx) instead of storing the whitespace-only value; reference
functions: UpdateTraceId, validateTraceID, SetTraceId, SetTraceIdWithValue.

---

Nitpick comments:
In `@notifier/notifier_test.go`:
- Around line 242-253: Add a new table test row to explicitly assert how
multibyte Unicode is handled: insert an entry alongside the existing cases
(e.g., next to "printable special chars preserved") such as {"unicode
preserved", "héllø—你好", "héllø—你好"} to verify the sanitizer preserves non-ASCII
printable Unicode characters; add or adjust expected output according to the
behavior of the sanitizer function used in these tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e3a723b6-cf3c-4b87-b7a6-f14604d53b4a

📥 Commits

Reviewing files that changed from the base of the PR and between e40523f and efc242f.

📒 Files selected for processing (4)
  • README.md
  • notifier/README.md
  • notifier/notifier.go
  • notifier/notifier_test.go

ankurs added 2 commits April 4, 2026 15:16
Add SetTraceIDValidator(func(string) string) to allow custom validation
or disable it entirely (set to nil). Follows the existing init-only
setter pattern. Default behavior unchanged (max 128 chars, printable
ASCII only).
Address review feedback:
- Add non-ASCII UTF-8 test case (accented, CJK, emoji) to validateTraceID
- Use strings.TrimSpace in UpdateTraceId to match SetTraceIdWithValue
  behavior — whitespace-only trace IDs now fall back to generation
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (1)

notifier/notifier.go:565

  • PR description states that when all characters are invalid the logic "falls back to generating a new UUID", but the code paths can fall back to OTEL span trace ID (and UpdateTraceId can fall back to SetTraceId, which may also pick up incoming gRPC metadata). Please update the PR description (or adjust behavior) so the documented fallback matches what ships.
	// Fall back to OTEL span trace ID.
	if strings.TrimSpace(traceID) == "" && hasSpan {
		traceID = span.SpanContext().TraceID().String()
	}
	// Last resort: generate UUID.
	if strings.TrimSpace(traceID) == "" {
		u, err := uuid.NewRandom()

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
notifier/notifier_test.go (2)

286-293: Unnecessary pre-initialization of trace ID.

Line 287 pre-sets an empty trace ID, but UpdateTraceId unconditionally overwrites it via options.AddToOptions. This setup is redundant and could be simplified.

♻️ Simplified test
 func TestUpdateTraceId_ValidatesInput(t *testing.T) {
-	ctx := options.AddToOptions(context.Background(), tracerID, "")
-	ctx = UpdateTraceId(ctx, "good-id\x00\nbad")
+	ctx := UpdateTraceId(context.Background(), "good-id\x00\nbad")
 	got := GetTraceId(ctx)
 	if got != "good-idbad" {
 		t.Errorf("expected sanitized trace ID %q, got %q", "good-idbad", got)
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@notifier/notifier_test.go` around lines 286 - 293, The test
TestUpdateTraceId_ValidatesInput redundantly pre-initializes the trace ID using
options.AddToOptions before calling UpdateTraceId, which always overwrites the
value; remove the initial options.AddToOptions call and create the context by
calling UpdateTraceId directly (e.g., ctx := UpdateTraceId(context.Background(),
"good-id\x00\nbad")), leaving the subsequent GetTraceId assertion unchanged to
verify sanitization.

303-334: Document that these tests must not run in parallel.

These tests modify the package-level traceIDValidator without synchronization (as documented: "Must be called during init — not safe for concurrent use"). While they currently don't use t.Parallel(), a future contributor adding it would introduce a data race.

Consider adding a brief comment at the top of the first validator test to prevent accidental parallelization.

📝 Add clarifying comment
+// TestSetTraceIDValidator_* tests modify package-level state and must not
+// run in parallel with each other or with tests that call SetTraceId/UpdateTraceId.
 func TestSetTraceIDValidator_Custom(t *testing.T) {
 	old := traceIDValidator
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@notifier/notifier_test.go` around lines 303 - 334, These tests mutate the
package-level traceIDValidator via SetTraceIDValidator (used in
TestSetTraceIDValidator_Custom and TestSetTraceIDValidator_Nil) and are not
concurrency-safe; add a short comment at the top of the validator tests (e.g.,
at the start of TestSetTraceIDValidator_Custom, and mirror in
TestSetTraceIDValidator_Nil) stating that the tests modify traceIDValidator and
must not be run in parallel (do not call t.Parallel()), so future contributors
don't accidentally parallelize them.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@notifier/notifier_test.go`:
- Around line 286-293: The test TestUpdateTraceId_ValidatesInput redundantly
pre-initializes the trace ID using options.AddToOptions before calling
UpdateTraceId, which always overwrites the value; remove the initial
options.AddToOptions call and create the context by calling UpdateTraceId
directly (e.g., ctx := UpdateTraceId(context.Background(), "good-id\x00\nbad")),
leaving the subsequent GetTraceId assertion unchanged to verify sanitization.
- Around line 303-334: These tests mutate the package-level traceIDValidator via
SetTraceIDValidator (used in TestSetTraceIDValidator_Custom and
TestSetTraceIDValidator_Nil) and are not concurrency-safe; add a short comment
at the top of the validator tests (e.g., at the start of
TestSetTraceIDValidator_Custom, and mirror in TestSetTraceIDValidator_Nil)
stating that the tests modify traceIDValidator and must not be run in parallel
(do not call t.Parallel()), so future contributors don't accidentally
parallelize them.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 90fdc3c4-2931-442d-a9af-cf4fec8beb27

📥 Commits

Reviewing files that changed from the base of the PR and between efc242f and d90ab7a.

📒 Files selected for processing (3)
  • notifier/README.md
  • notifier/notifier.go
  • notifier/notifier_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • notifier/notifier.go

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@ankurs ankurs merged commit c7a2b89 into main Apr 4, 2026
11 checks passed
@ankurs ankurs deleted the fix/validate-trace-ids branch April 4, 2026 09:17
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