Release Candidate: 8.1.13 - #6174
Merged
Merged
Conversation
…names as method calls Backport to the 8.1 backport branch.
…port) Backport of the security fix that limits how long one in-memory query may spend executing regular expressions. Adds Matcher::RegexpBudget, a configurable limit bounded across embedded-association queries, association removal, and Document#_matches?.
Backport of the security fix to 8.1. An id in nested attributes was resolved against the parent's association, and on a miss it fell back to a collection-wide unscoped lookup that dropped default scopes. The matched document could then be mass assigned and pushed into the caller's association, letting a user overwrite someone else's document. The id is now resolved against the association's class. Non-scalar ids (operator hashes, arrays) are rejected. Any id that is not already in the association raises a DocumentNotFound error, and a destroy of a document that is not in the association is ignored. Introduces allow_reparenting_via_nested_attributes (defaulting to false, from MONGOID-5930) as an upgrade path for applications that relied on reparenting. Enabling it restores the previous behavior behind a one-time deprecation warning.
…8.1 backport) Backport 5993 to 8.1, along with the prerequisite pieces of MONGOID-5939 it depends on: the allow_unsafe_query_operators config option, the expr_query allowlist guard, and the string-form guard on Selectable#where. Skipped 5939's unrelated Gemfile change.
…port)
The allowlist added in MONGOID-5939 inspected only the top-level keys of a criterion, and only #where consulted it. The other entry points into the selector -- and, or, nor, not, any_of, none_of, elem_match -- reach it through Mergeable and bypassed the guard entirely, so or('$where' => js) still ran with strict mode on. Nested forms such as {'$expr' => {'$function' => ...}} and {'$or' => [{'$where' => ...}]} were likewise invisible to a top-level check.
Move the guard to _mongoid_expand_keys, the one point every user-supplied expression passes through, and add a second rule: $where, $function, and $accumulator are rejected at any depth. The top-level allowlist is unchanged; recursing with it would reject $gt, $in, and every aggregation operator legitimately used inside $expr.
Flip allow_unsafe_query_operators to default to false. It is deliberately not wired into load_defaults, so an application on older defaults still gets the guard and has to opt out explicitly.
Criteria#for_js is unaffected when called directly, since js_query does not go through this funnel, but it now raises when merged into another criteria via or/any_of, which re-expand the merged selector. The string form of #where still compiles to $where; that path is MONGOID-5993.
…or smuggling (8.1)
The 5994 guard lives in _mongoid_expand_keys, but elem_match (and every other
entry point routed through Mergeable#__override__ -- exists, eq, gt, in, ne,
geo, etc.) writes to the selector directly and never passes through the
funnel. With overwrite_chained_operators set to true (the pre-8.0 default,
still active for applications that enable the legacy behavior), elem_match(a:
{ '$where' => js }) therefore bypasses strict mode.
Enforce _mongoid_validate_operators! at the top of __override__, matching the
7.6 backport. The default suite does not exercise this path (the flag
defaults to false on 8.1, routing elem_match through and_with_operator), so
no test coverage is added here.
The existing elem_match guard case runs under the branch default of overwrite_chained_operators=false, so it exercises and_with_operator, not the Mergeable#__override__ path that the guard was added to. Add a case that forces the flag true, asserting elem_match with a nested $where is rejected under strict mode, that an ordinary value still works, and that opting out restores the old behavior.
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical and moderate findings remain in query validation, legacy field access, regexp budgeting, and nested-association handling.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Release candidate for Mongoid 8.1.13, adding safeguards for in-memory matching, query operators, nested attributes, and field access.
Changes:
- Adds cumulative regexp timeouts and safer in-memory field reads.
- Restricts unsafe query operators by default.
- Scopes nested-attribute resolution to associations and updates release metadata.
File summaries
| File | Summary |
|---|---|
spec/mongoid/matcher/regexp_budget_spec.rb |
Regexp budget coverage |
spec/mongoid/criteria/queryable/selectable_where_spec.rb |
String-query guard coverage |
spec/mongoid/criteria/queryable/selectable_logical_spec.rb |
Logical operator guard coverage |
spec/mongoid/criteria_spec.rb |
Criteria query coverage |
spec/mongoid/contextual/memory_spec.rb |
Safe in-memory field access |
spec/mongoid/contextual/aggregable/memory_spec.rb |
Safe aggregation access |
spec/mongoid/attributes/nested_spec.rb |
Nested attribute behavior |
spec/mongoid/association/referenced/has_many/proxy_spec.rb |
Association removal behavior |
spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb |
HABTM removal behavior |
spec/integration/query_operator_guard_spec.rb |
Query operator guard integration tests |
spec/integration/matcher_regexp_timeout_spec.rb |
Regexp timeout integration tests |
spec/integration/matcher_operator_data/regex.yml |
Regex matcher fixtures |
spec/integration/dots_and_dollars_spec.rb |
Dollar-field compatibility |
spec/integration/associations/has_and_belongs_to_many_spec.rb |
Reparenting behavior |
spec/integration/app_spec.rb |
JSON compatibility |
product.yml |
Release metadata |
lib/mongoid/warnings.rb |
Reparenting warning |
lib/mongoid/version.rb |
Version 8.1.13 |
lib/mongoid/threaded.rb |
Regexp budget thread state |
lib/mongoid/matcher/regexp_budget.rb |
Regexp budget implementation |
lib/mongoid/matcher/regex.rb |
Budgeted regex matching |
lib/mongoid/matcher/eq_impl_with_regexp.rb |
Regexp equality matching |
lib/mongoid/matcher.rb |
Matcher integration |
lib/mongoid/matchable.rb |
Matching budget integration |
lib/mongoid/field_readable.rb |
Data-only field reads |
lib/mongoid/errors/in_memory_regexp_timeout.rb |
Timeout exception |
lib/mongoid/errors.rb |
Error registration |
lib/mongoid/criteria/queryable/selectable.rb |
Query operator validation |
lib/mongoid/criteria/queryable/mergeable.rb |
Chained operator guards |
lib/mongoid/contextual/memory.rb |
In-memory matching and reads |
lib/mongoid/contextual/aggregable/memory.rb |
Safe aggregation reads |
lib/mongoid/config.rb |
New configuration options |
lib/mongoid/association/referenced/has_many/proxy.rb |
Bounded association removal |
lib/mongoid/association/nested/nested_buildable.rb |
Nested ID validation |
lib/mongoid/association/nested/many.rb |
Association-scoped nested IDs |
lib/mongoid/association/depending.rb |
Dependency owner naming |
lib/config/locales/en.yml |
Timeout messages |
Review details
Suppressed comments (4)
lib/mongoid/association/nested/many.rb:221
- For a referenced
has_manywith a customprimary_key, the association membership query usesbase.send(primary_key)(seehas_many.rb:113-115), but this error reportsparent.id. The newDocumentNotFoundparams therefore identify the wrong parent key—for example,_idinstead ofparent.p; use the association's configured primary key value.
{ _id: id, association.foreign_key => parent.id }
lib/mongoid/association/nested/many.rb:186
- This branch runs before
destroyable?, so an_destroyrequest for an id absent from an embedded many association raisesDocumentNotFoundinstead of being ignored. Move the missing-association destroy check ahead of the embedded not-found check to keep the documented behavior consistent for embedded and referenced associations.
elsif association.embedded?
raise Errors::DocumentNotFound.new(association.klass, id)
elsif destroyable?(attrs)
# A destroy of a document that is not in the association is
# ignored, rather than reaching for it outside the association.
lib/mongoid/config.rb:229
- The behavior described here is not true for all Ruby 3.2+ implementations:
RegexpBudget::PER_REGEXP_TIMEOUTis enabled only for MRI, while the code explicitly uses theTimeoutfallback on JRuby even when it reports Ruby 3.4. Please document this as MRI 3.2+ (or otherwise mention the engine capability) so users know the configured limit may cover wall-clock evaluation on JRuby.
# Set to nil to remove the limit. On Ruby 3.2 and later the remaining
# budget is compiled into the pattern, so the limit counts only the time
# spent matching. Earlier Rubies have no per-Regexp timeout, so the query
# is bounded with Timeout instead and the limit is wall clock over the
# whole in-memory evaluation.
lib/mongoid/matcher/regexp_budget.rb:139
- Rebuilding an application-supplied
Regexphere discards its per-pattern timeout and replaces it with the Mongoid budget/global timeout. Thus a caller that set a stricter timeout on the condition is silently less protected whenever a budget is open, unlike the global-timeout behavior tested in this PR. Preserve the smaller per-pattern timeout (and include it in the cache key so equal source/options with different timeouts are not conflated).
::Regexp.new(regexp.source, regexp.options, timeout: @timeout)
- Files reviewed: 37/37 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| include Association::EagerLoadable | ||
| include Queryable | ||
| include Positional | ||
| include FieldReadable |
Comment on lines
+878
to
880
| # The operator guard is applied by _mongoid_expand_keys, which every | ||
| # query method that accepts a user-supplied expression passes through. | ||
| normalized = _mongoid_expand_keys(criterion) |
Comment on lines
+221
to
+229
| if budget.nil? || PER_REGEXP_TIMEOUT | ||
| yield | ||
| else | ||
| begin | ||
| Timeout.timeout(budget.limit, TimedOut, &block) | ||
| rescue TimedOut | ||
| raise timeout_error(budget) | ||
| end | ||
| end |
Matches master's pin. The pinned 14cc285a selects the ASAN/UBSAN-instrumented Python 3.14 via find_python3 on refreshed ubuntu2404 images; LeakSanitizer then aborts the mongo-orchestration bootstrap on pymongo _cmessage import-time leaks, failing every test task on that matrix before specs run. 890a93b (DRIVERS-3564 uv rework) builds the orchestration venv from 'python' on PATH instead of the /opt/python version scan, avoiding the sanitizer build.
Jibola
approved these changes
Sep 17, 2026
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.
The MongoDB Ruby team is pleased to announce version 8.1.13 of the
mongoidgem - a Ruby ODM for MongoDB. This is a new patch release in the 8.1.x series of Mongoid.Install this release using RubyGems via the command line as follows:
Or simply add it to your
Gemfile:Have any feedback? Click on through to MongoDB's Jira and open a new ticket to let us know what's on your mind.
Bug Fixes
Bound regular-expression execution time in in-memory queries (MONGOID-5981)
Queries evaluated in memory, such as those against embedded associations, run in the calling thread and can spend an unbounded amount of CPU matching regular expressions. A single in-memory query is now limited by
Mongoid.in_memory_regexp_time_limit(default5.0seconds); exceeding the limit raisesMongoid::Errors::InMemoryRegexpTimeout.Resolve nested attribute ids within the caller's association (MONGOID-5992)
An id given in nested attributes is now resolved within the association the attributes are being applied to, rather than falling back to a collection-wide lookup that ignored default scopes. A document that is not part of that association raises
Mongoid::Errors::DocumentNotFound, and a request to destroy a document that is not in the association is ignored. The newMongoid.allow_reparenting_via_nested_attributesoption (defaultfalse) restores the previous reparenting behavior when set totrue.Reject the string form of where under the query operator guard (MONGOID-5993)
A String passed to
#whereis sent to MongoDB as a$whereexpression. This now raisesMongoid::Errors::InvalidQuerywhenMongoid.allow_unsafe_query_operatorsisfalse(the default); the string form is allowed only when that option is enabled.Reject JavaScript query operators at any depth (MONGOID-5994)
Mongoid.allow_unsafe_query_operatorsnow defaults tofalse. When it isfalse, the$where,$function, and$accumulatoroperators are rejected anywhere in a query selector. The guard covers every criterion-building method (where,find_by,or,and,nor,not,any_of,none_of, andelem_match, including chained operator overrides) and inspects nested expressions such as{'$expr' => {'$function' => ...}}in full.Other Bug Fixes