Skip to content

fix: Stop per-context private attributes from accumulating between contexts - #416

Merged
kinyoklion merged 1 commit into
mainfrom
devin/1786660571-private-attribute-leak
Aug 18, 2026
Merged

fix: Stop per-context private attributes from accumulating between contexts#416
kinyoklion merged 1 commit into
mainfrom
devin/1786660571-private-attribute-leak

Conversation

@kinyoklion

@kinyoklionkinyoklion commented Aug 13, 2026

Copy link
Copy Markdown
Member

Requirements

  • I have added test coverage for new or changed functionality
  • I have followed the repository's pull request submission guidelines
  • I have validated my changes against all supported platform versions

Related issues

Found while investigating the contract test failures fixed in #415. Not covered by the current contract test suite.

Describe the solution you've provided

ContextFilter#filter_single_context combined the globally configured private attributes with the context's own _meta.privateAttributes using Array#concat, which mutates @private_attributes in place. Every context filtered by a ContextFilter therefore permanently added its private attributes to the filter's configuration, so a context's private attributes were applied to all contexts filtered afterwards — including the other kinds of the same multi-kind context.

Before (single ContextFilter.new(false, [])):

filter.filter(user_with_private_email)# {..., _meta: {redactedAttributes: [:email]}}filter.filter(other_user)# {..., _meta: {redactedAttributes: [:email]}} <- email was not private here

The fix builds a new array (@private_attributes + context.private_attributes) instead of mutating the configured list. Event processors reuse a single ContextFilter for the lifetime of the client, so the leak was cumulative across all events.

Describe alternatives you've considered

Constructing the filter per event — unnecessary allocation churn, and the mutation is the actual defect.

Additional context

Full contract test suite against harness v3.2.0-alpha.6 passes with this change (combined with #415): 4723 total, 14 skipped, all ran passed.

Link to Devin session: https://app.devin.ai/sessions/316afaccd2604f8d802a72c9080f6921
Requested by: @kinyoklion


Note

Overview
Fixes a privacy bug in ContextFilter#filter_single_context: merging global private attributes with each context’s _meta.privateAttributes used Array#concat, which mutated the filter’s @private_attributes. Because EventOutputFormatter keeps one ContextFilter for the client lifetime, later contexts (and other kinds in the same multi-kind context) could incorrectly redact attributes that were only private on a previous context.

The change uses @private_attributes + context.private_attributes so the configured list is never mutated. New specs cover sequential filtering and multi-kind isolation.

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

Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
@kinyoklionkinyoklion self-assigned this Aug 13, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration

Copy link
Copy Markdown
Contributor

@cursor review

@kinyoklion
kinyoklion marked this pull request as ready for review August 17, 2026 17:56
@kinyoklion
kinyoklion requested a review from a team as a code ownerAugust 17, 2026 17:56
@kinyoklionkinyoklion changed the title fix: Stop per-context private attributes from leaking between contextsfix: Stop per-context private attributes from accumulating between contextsAug 17, 2026
@kinyoklion
kinyoklion merged commit 2fd26ea into mainAug 18, 2026
13 checks passed
@kinyoklion
kinyoklion deleted the devin/1786660571-private-attribute-leak branch August 18, 2026 16:46
kinyoklion added a commit to launchdarkly/sdk-test-harness that referenced this pull request Aug 19, 2026
…contexts (#430)
Adds contract coverage for private attributes declared in a context's
`_meta.privateAttributes` leaking into an SDK's *globally configured*
private attribute list, so they get applied to unrelated contexts.
- Port of #429 to the `v2` line. Nearly every SDK's CI pulls the harness
from `v2` (ruby, php, erlang, rust, haskell, java, dotnet, ios, android,
flutter, roku, node-client), so without this the regression below is
untested for those SDKs.
- 3 new subtests under `events/context properties`; runs for
server-side, client-side and PHP suites. No existing test or expectation
changed.
- Reproduces
[ruby-server-sdk#416](launchdarkly/ruby-server-sdk#416):
`ContextFilter` did
`@private_attributes.concat(context.private_attributes)`, mutating the
configured list, so a context that declared nothing private still had
the *previous* context's private attributes redacted.
**Requirements**
- [x] I have added test coverage for new or changed functionality
- [x] I have followed the repository's [pull request submission
guidelines](../blob/master/CONTRIBUTING.md#submitting-pull-requests)
- [ ] I have validated my changes against all supported platform
versions — validation across SDKs in progress, results posted as a
comment
**Related issues**
- launchdarkly/ruby-server-sdk#416 — the bug this covers
- #429 — the same change on the `v3` line
<details>
<summary>Implementation details</summary>
**Why the existing tests could not catch it**
`makeEventContextTestParams` creates a *new client* per parameter, so
the `ContextFilter` is always fresh, and every context within one
parameter comes from a single factory with identical
`_meta.privateAttributes`. A leaked private attribute is therefore
always an already-expected private attribute. The existing multi-kind
fixtures don't catch it either: they either set no per-context privates,
or set them only on the kind that is filtered last, and the leaked names
don't exist as attributes on the other kind.
**What the new tests do**
`eventContextPrivateAttributeScoping` uses **one client** for multiple
identify events, and each context carries the same three attributes
(`selfPrivate`, `globallyPrivate`, `visible`) while differing only in
what it declares private:
1. `private attributes of one context are not applied to later contexts`
— identify a context declaring `selfPrivate` private, then identify a
different context that declares nothing private, and assert
`selfPrivate` is still visible on the second one.
2. + 3. `private attributes of one kind are not applied to other kinds
of the same context (declared by org / by user)` — a multi-kind context
where only one kind declares `selfPrivate` private; the other kind must
keep it. Both orderings are covered because SDKs filter the individual
contexts in an arbitrary order — with the Ruby bug present, only the
`org` variant fails (Ruby filters `org` first), so a single-ordering
test would catch this only half the time.
`globallyPrivate` is configured via `GlobalPrivateAttributes` and
asserted redacted in every expectation, so an SDK cannot pass by
throwing away its configured private attributes along with the
per-context ones.
**Verification**
Negative control against ruby-server-sdk with the #416 fix locally
reverted to `.concat(...)`: subtests 1 and 2 fail with the second
context wrongly reporting `redactedAttributes:
["selfPrivate","globallyPrivate"]`; all three pass with the fix in
place.
</details>
Link to Devin session:
https://app.devin.ai/sessions/6e3076285f2849919b966a4f801075ca
Requested by: @kinyoklion
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Overview**
> Adds **three new subtests** under `events/context properties` via
`eventContextPrivateAttributeScoping`, hooked from `EventContexts`.
Existing expectations are unchanged.
> > The harness reuses **one SDK client** for multiple identify events so
failures like mutating the global private-attribute list (e.g.
ruby-server-sdk#416) are detectable—prior cases mostly created a fresh
client per scenario.
> > **Coverage:** (1) a context that marks `selfPrivate` private must not
cause a later context with no per-context privates to redact
`selfPrivate`; (2) for multi-kind contexts, privates declared on `org`
or `user` must not redact the same attribute on the other kind—both
orderings are tested.
> > Every assertion still expects **`globallyPrivate`** (from
`GlobalPrivateAttributes`) to be redacted so SDKs cannot pass by
dropping all configured privates.
> > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
3fa29c4. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Co-authored-by: Devin AI <devin@launchdarkly.com>
kinyoklion added a commit to launchdarkly/sdk-test-harness that referenced this pull request Aug 19, 2026
…contexts (#429)
Adds `events/context properties` coverage for private attributes
declared in a context's `_meta.privateAttributes` leaking onto other
contexts — the Ruby SDK bug fixed in launchdarkly/ruby-server-sdk#416,
which the existing suite could not detect.
- New subtest: two identify events on one client, the first context
declaring `selfPrivate` private, the second not — the second must not
have it redacted.
- New subtests (both kind orderings): one multi-kind context where only
one kind declares `selfPrivate` private — the other kind must not have
it redacted.
- A globally configured private attribute is expected redacted in every
assertion, so an SDK cannot pass by dropping its configured private
attributes along with the per-context ones.
- No new capability gate: this is required behavior for every SDK, so
all SDKs run it.
**Requirements**
- [x] I have added test coverage for new or changed functionality
- [x] I have followed the repository's [pull request submission
guidelines](../blob/master/CONTRIBUTING.md#submitting-pull-requests)
- [ ] I have validated my changes against all supported platform
versions
<details>
<summary>Implementation details</summary>
**Why existing tests miss it**
`makeEventContextTestParams` builds one client per test param, and every
context that param then generates comes from a single
`data.ContextFactory` with identical `_meta.privateAttributes`. An SDK
that merges per-context private attributes into its long-lived
configured list (rather than a per-context copy) therefore leaks only
attributes that are already expected to be redacted. The multi-kind
factories are also blind to it: they either declare no per-context
privates, or declare them only on the kind filtered last
(`data.NewContextFactoriesForExercisingAllAttributes` gives `org` just
`name` while `other` carries `Private("a","c")`), and the leaked names
do not exist on the other kind.
**What the new tests do**
`CommonEventTests.eventContextPrivateAttributeScoping` is called at the
end of `EventContexts`, so it runs for server-side, client-side, and PHP
SDKs. Each context carries `selfPrivate`, `globallyPrivate`, and
`visible`; `globalPrivateAttributes` is configured as
`["globallyPrivate"]`.
- Sequential case: identify `selfPrivate`-declaring context (expect
`selfPrivate` + `globallyPrivate` redacted), then identify a context
that declares nothing private (expect only `globallyPrivate` redacted).
Pre-#416 Ruby redacts `selfPrivate` from the second context too.
- Multi-kind case: `org` + `user` in one context, only one kind
declaring `selfPrivate` private; run once per kind so the leak is caught
regardless of the order an SDK filters individual contexts in.
Both cases still assert `globallyPrivate` is redacted, which is what
distinguishes a correct fix (copy the list) from a wrong one (stop
applying configured privates).
**Testing**
`make build`, `make lint` (0 issues), and `make test` pass. SDK-level
validation is in progress: the new subtests are being run against SDK
contract test services, including the Ruby SDK before and after #416.
**Alternatives considered**
Adding more `eventContextTestParams` entries — rejected, the leak is
only observable across two contexts with *different* private attributes
within one client, which that table cannot express.
</details>
Link to Devin session:
https://app.devin.ai/sessions/6e3076285f2849919b966a4f801075ca
Requested by: @kinyoklion
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Overview**
> Adds **`eventContextPrivateAttributeScoping`** at the end of
**`EventContexts`**, so server-side, client-side, and PHP SDKs all run
it.
> > The new coverage catches SDKs that merge **`_meta.privateAttributes`**
into a long-lived global list instead of a per-context copy (e.g. Ruby
server SDK before #416). A **sequential identify** case sends one
context that marks **`selfPrivate`** private, then another that does
not—only **`globallyPrivate`** (from **`globalPrivateAttributes`**) must
be redacted on the second event. **Multi-kind** subtests run for both
**`org`** and **`user`** as the kind that declares **`selfPrivate`**, so
redaction cannot leak across kinds when filtering one kind at a time.
> > Every assertion still expects **`globallyPrivate`** redacted, so
passing by dropping all configured private attributes along with
per-context ones fails the suite.
> > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
62615b4. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Co-authored-by: Devin AI <devin@launchdarkly.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@kinyoklion@keelerm84