MONGOID-5991 Filter nil keys out of association membership queries - #6169
Open
jamis wants to merge 1 commit into
Open
MONGOID-5991 Filter nil keys out of association membership queries#6169jamis wants to merge 1 commit into
jamis wants to merge 1 commit into
Conversation
HasManyThrough#criteria dropped the plucked key values straight into an $in. An intermediate document missing the plucked key contributes a nil, and a nil in an $in matches every document whose queried field is null or absent. With a custom primary key those are real documents, so the association read returned documents outside the relationship, including documents belonging to another owner. Both branches of #criteria now compact and uniq the plucked values, which is what the eager loader already did via filter_map. The lazy and eager paths no longer disagree on the same data. An empty array after filtering yields an empty $in, which correctly matches nothing. The same shape appears in has_and_belongs_to_many, built from the stored foreign key array rather than a pluck. Associating a target whose custom primary key is unset stores a literal nil there and reproduces the leak, so compact that array too. The id_list argument is not guaranteed to be an Array, so the compact is guarded.
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes an association query correctness/security issue where nil values could enter $in membership queries for custom primary keys, causing unrelated documents (including cross-owner) to be returned. This brings lazy association loading behavior in line with the existing eager loader behavior.
Changes:
HasManyThrough#criterianow compacts and de-duplicates plucked key values via a helper before building$inselectors.- HABTM membership querying now compacts stored foreign key arrays (when the list is an Array) before building the
$inselector. - Adds integration specs covering both HasManyThrough branches and HABTM, including “all keys nil” and cross-owner leak scenarios.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| lib/mongoid/association/referenced/has_many_through.rb | Filters nil (and de-dupes) plucked intermediate key values before $in membership queries. |
| lib/mongoid/association/referenced/has_and_belongs_to_many.rb | Compacts stored foreign key arrays (when Array) to prevent nil from entering $in queries. |
| spec/mongoid/association/referenced/has_many_through_spec.rb | Adds integration coverage for missing plucked keys in both HasManyThrough criteria branches, ensuring no leakage and eager/lazy agreement. |
| spec/mongoid/association/referenced/has_and_belongs_to_many_spec.rb | Adds integration coverage for nil stored keys in HABTM foreign key arrays, ensuring no leakage and correct empty results. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
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.
Description
HasManyThrough#criteriabuilt its membership query by plucking key values from the intermediate criteria and dropping the result straight into an$in, with no filtering. An intermediate document missing the plucked key contributes a literalnil, and anilin an$inmatches every target document whose queried field is null or absent. The association read then returned documents that are not in the relationship, including documents belonging to another owner.Both branches of
#criteriawere affected. The eager loader already did the right thing (filter_mapinhas_many_through/eager.rb), so the lazy and eager paths returned different results for identical data — which is what makes this an omission rather than a design choice.The precondition is a custom primary key. With the default
_id,nilstill enters the$in, but no document has a null or absent_id, so nothing matches and nothing leaks.The fix
Both branches now route plucked values through a
key_valueshelper that appliescompactanduniq, mirroring the eager loader.compactis the fix;uniqshrinks the query and makes the two paths structurally identical. If filtering leaves the array empty, an empty$incorrectly matches nothing — the right outcome for an association whose intermediates carry no usable key.has_and_belongs_to_many
has_and_belongs_to_many.rbbuilds the sameprimary_key => $in id_listshape from the stored foreign-key array. Associating a target whose custom primary key is unset stores a literalnilin that array and reproduces the same leak. That array is now compacted too.One note on this half:
id_listis not guaranteed to be anArray— an existing spec passes a class — so thecompactis guarded with anis_a?(Array)check rather than changing that spec's contract. Thenilid_list →crit.nonebranch is untouched, so "no list" and "list of only nils" remain distinguishable.Tests
11 new examples, each watched fail before the fix was written:
nilin the emitted selector, and that the lazy and eager paths agree on identical data. Plus an all-keys-nil case (returns nothing, not everything) and the cross-owner leak with an orphaned target.The lazy/eager agreement specs are redundant with the leak specs today. They are included because the divergence between the two paths is what made this defect possible, and they are the cheapest guard against reintroducing it.
Verification
spec/mongoid/association/referenced/— 3466 examples, 0 failures, 7 pending (pre-existing).lib/reverted and pass with the fix.