Skip to content

fix: Escape attribute names reported in redactedAttributes - #228

Open
jsonbailey wants to merge 3 commits into
mainfrom
jb/sdk-3216/redacted-attr-escaping
Open

jsonbailey wants to merge 3 commits into
mainfrom
jb/sdk-3216/redacted-attr-escaping

Conversation

@jsonbailey

@jsonbailey jsonbailey commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

The bug

When an attribute is redacted wholesale, the SDK reported the raw attribute name in _meta.redactedAttributes instead of an escaped attribute reference. A literal attribute named /ssn was reported as /ssn, which a consumer parses as a path to a nested property ssn — so the redaction of the top-level /ssn attribute was never communicated.

No attribute value was ever leaked. Only the reported reference string was wrong. The correct form is /~1ssn.

This affected the two paths that redact a whole attribute without consulting a configured reference:

  • allAttributesPrivate is set
  • the context is anonymous and anonymous redaction is enabled

The fix

In writeOrRedactAttribute, wrap the name before reporting it:

return addOrCreate(redacted, AttributeRef.fromLiteral(attrName).toString());

AttributeRef is unchanged — it was already correct. fromLiteral escapes a leading slash (/ssn → /~1ssn, /a~b → /~1a~0b) and leaves any other name alone, since a name without a leading slash is already a valid reference. That behavior already had test coverage in AttributeRefTest.literal().

The two duplicate branches were also collapsed into one condition, since both arms did the same thing.

Why only one call site changed

There are three addOrCreate(redacted, ...) sites in EventContextFormatter:

Line Site Status
64 addOrCreate(redacted, "name") Correct — the literal "name" can never need escaping
110 writeOrRedactAttribute Fixed here
136 writeRedactedValue, depth-1 private match Correct — already reports privateRef.toString()

Line 136 handles the explicitly-configured-private case and was never broken: it reports the configured reference rather than the attribute name, and AttributeRef always stores its rawPath pre-escaped regardless of how it was built.

Note for reviewers on the test count: four test cases were added, but only two were failing beforehand. The two covering explicitly-configured private attributes (fromLiteral("/ssn") as a global private attribute, and .privateAttributes("/~1ssn") per-context) pass against the pre-fix source as well — they are regression guards for the already-correct line 136, not evidence of a second fix.

Test evidence

lib/shared/internal: 318 tests pass, 0 failures, checkstyleMain and checkstyleTest clean.

Revert check: restoring the pre-fix EventContextFormatter.java while keeping the new tests fails exactly the two new escaping cases, with the raw-vs-escaped difference explicit:

all attributes private globally - names needing escaping
  at "_meta.redactedAttributes[0]": expected = "/~1a~0b", actual = "/a~b"
redacting anonymous context - names needing escaping
  at "_meta.redactedAttributes[0]": expected = "/~1ssn",  actual = "/ssn"

EventContextFormatterTest also gained a redactAnonymous parameter. It previously hardcoded write(context, jw, false), so the anonymous-redaction branch had no direct coverage in this formatter's own test.

Not verifiable in CI yet

The 14 contract tests in #203 cannot go green from this branch. lib/sdk/server/build.gradle:74 pins launchdarklyJavaSdkInternal = "1.11.1" and line 115 consumes it as a published Maven artifact — the server SDK does not build lib/shared/internal from source. Running the contract tests against this branch would exercise the released 1.11.1, not this change.

Going green requires a java-sdk-internal release followed by a version bump in the server SDK. Neither is done here, since version changes belong to the release process.

Prior art

The same defect was fixed in two sibling SDKs:

Python additionally had to fix its from_literal helper, which did not escape a leading slash. Java's fromLiteral was already correct, so no equivalent change was needed. .NET needed two call-site fixes because its WriteOrRedact short-circuits with a raw name before reaching the equivalent of writeRedactedValue; Java falls through to privateRef.toString(), which is why one fix suffices here.

SDK-3216


Note

Overview
Fixes _meta.redactedAttributes so wholesale redaction (global all attributes private or anonymous contexts when anonymous redaction is on) records escaped attribute references instead of raw names.

In writeOrRedactAttribute, redacted entries now use AttributeRef.fromLiteral(attrName).toString() (e.g. /ssn → /~1ssn), so consumers do not misread a top-level slash-prefixed attribute as a nested path. The two identical redaction branches are merged into one condition.

EventContextFormatterTest adds a redactAnonymous parameter (tests previously always passed false) and new cases for slash/~ attribute names, anonymous redaction, and configured-private slash-prefixed attributes as regression guards.

Reviewed by Cursor Bugbot for commit 3011d66. Bugbot is set up for automated code reviews on this repo. Configure here.

When a whole attribute is redacted because allAttributesPrivate is set, or
because the context is anonymous and anonymous redaction is enabled, the raw
attribute name was reported in _meta.redactedAttributes. A name that starts
with a slash, such as "/ssn", was therefore reported as "/ssn", which a
consumer reads as a path to a nested property "ssn". The redaction of the
top-level attribute was not communicated. It is now reported as the escaped
attribute reference "/~1ssn".

Only the reported reference was wrong. The attribute value was never sent.
The depth-1 private-reference match already reports the configured reference,
which AttributeRef always stores in escaped form. This case guards that
behavior for a per-context private attribute.
@jsonbailey
jsonbailey marked this pull request as ready for review September 29, 2026 20:46
@jsonbailey
jsonbailey requested a review from a team as a code owner September 29, 2026 20:46

This branch has not been deployed

No deployments
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.

1 participant