Release Candidate: 7.6.2 - #6176
Merged
Merged
Conversation
…names as method calls
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 7.6. 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.
…7.6 backport) Backport 5993 to 7.6, 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)
Backport the MONGOID-5994 operator guard to 7.6. The guard now runs in
_mongoid_expand_keys, the one point every user-supplied query expression
passes through, so and, or, nor, not, any_of, and elem_match can no longer
smuggle in $where. Nested forms such as {'$expr' => {'$function' => ...}}
and {'$or' => [ {'$where' => ...} ]} are also rejected.
allow_unsafe_query_operators now defaults to false.
Prerequisite work from MONGOID-5939 shipped with the 5993 backport, so all
files except the deleted spec/integration/dots_and_dollars_spec.rb (absent
from 7.6) merged cleanly.
Squashed commit of the following:
commit 368f0b2f78b946a6ecaae002f2c347f7e3994dc4
Author: Jamis Buck <jamis.buck@mongodb.com>
Date: Fri Sep 11 09:02:47 2026 -0600
MONGOID-5994 Guard elem_match and chained overrides against JS operator smuggling (7.6)
The 5994 guard lives in _mongoid_expand_keys, but on this branch elem_match
(and every other entry point that routes through Mergeable#__override__ --
exists, eq, gt, in, ne, geo, etc.) writes to the selector directly and never
passes through the funnel. Under the default overwrite_chained_operators=true
setting, elem_match(a: { '$where' => js }) therefore bypassed strict mode.
Enforce _mongoid_validate_operators! at the top of __override__ so the
chained-override family is covered too. Master closes this gap by routing
elem_match through and_with_operator (MONGOID-5509/5330 removed the alternate
overwrite mode entirely), which is too invasive to backport wholesale; this is
the minimal equivalent for 7.6.
commit fa74842d157382f23b7b40f3df5d30a2fc85ba02
Author: Jamis Buck <jamis.buck@mongodb.com>
Date: Fri Sep 11 08:39:12 2026 -0600
MONGOID-5994 Reject JavaScript query operators at any depth (7.6 backport)
Backport the MONGOID-5994 operator guard to 7.6. The guard now runs in
_mongoid_expand_keys, the one point every user-supplied query expression
passes through, so and, or, nor, not, any_of, and elem_match can no longer
smuggle in $where. Nested forms such as {'$expr' => {'$function' => ...}}
and {'$or' => [ {'$where' => ...} ]} are also rejected.
allow_unsafe_query_operators now defaults to false.
Prerequisite work from MONGOID-5939 shipped with the 5993 backport, so all
files except the deleted spec/integration/dots_and_dollars_spec.rb (absent
from 7.6) merged cleanly.
Squashed commit of the 8.0 backport (a48a33c).
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Critical query-validation, regexp-budget, and unsafe field-access issues remain unresolved, along with lost CI coverage.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Release candidate for Mongoid 7.6.2, adding regexp limits, query-operator safeguards, safer in-memory access, and nested-attribute handling.
Changes:
- Adds cumulative in-memory regexp budgets and timeout errors.
- Rejects unsafe JavaScript query operators by default.
- Restricts nested-attribute reparenting and improves field access safety.
- Updates release metadata, tests, fixtures, and Evergreen configuration.
File summaries
| File | Reviewed change |
|---|---|
spec/mongoid/matcher/regexp_budget_spec.rb |
Regexp budget coverage. |
spec/mongoid/criteria/queryable/selectable_where_spec.rb |
Query guard coverage. |
spec/mongoid/criteria/queryable/selectable_logical_spec.rb |
Logical query and string-query updates. |
spec/mongoid/criteria_spec.rb |
Criteria compatibility coverage. |
spec/mongoid/contextual/memory_spec.rb |
Safe in-memory field access coverage. |
spec/mongoid/contextual/aggregable/memory_spec.rb |
Safe aggregation coverage. |
spec/mongoid/attributes/nested_spec.rb |
Nested-attribute behavior coverage. |
spec/mongoid/association/referenced/has_many/proxy_spec.rb |
Association removal coverage. |
spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb |
HABTM removal coverage. |
spec/integration/query_operator_guard_spec.rb |
Query-operator guard integration coverage. |
spec/integration/matcher_regexp_timeout_spec.rb |
Regexp timeout integration coverage. |
spec/integration/matcher_operator_data/regex.yml |
Regexp matcher fixtures. |
spec/integration/app_spec.rb |
JSON dependency compatibility. |
product.yml |
Product release metadata. |
lib/mongoid/warnings.rb |
Reparenting warning. |
lib/mongoid/version.rb |
Version 7.6.2 metadata. |
lib/mongoid/threaded.rb |
Moderate (2 votes): regexp budget thread-local key is not namespaced. |
lib/mongoid/matcher/regexp_budget.rb |
Critical (2 votes): caller regexp timeouts are not preserved. Moderate (2 votes): the budget is not checked after the final charge. |
lib/mongoid/matcher/regex.rb |
Budgeted regexp matching. |
lib/mongoid/matcher/eq_impl_with_regexp.rb |
Budgeted regexp equality matching. |
lib/mongoid/matcher.rb |
Matcher loading. |
lib/mongoid/matchable.rb |
Budgeted document matching. |
lib/mongoid/field_readable.rb |
Safe field resolution. |
lib/mongoid/errors/in_memory_regexp_timeout.rb |
In-memory regexp timeout error. |
lib/mongoid/errors.rb |
Error loading. |
lib/mongoid/criteria/queryable/selectable.rb |
Critical (3 votes): multi-criteria any_of can bypass unsafe-operator validation. Moderate (1 vote): string where changes the expected embedded-query error. |
lib/mongoid/criteria/queryable/mergeable.rb |
Query validation integration. |
lib/mongoid/contextual/memory.rb |
Critical (1 vote): the default legacy pluck path can still dispatch methods such as destroy. |
lib/mongoid/contextual/aggregable/memory.rb |
Safe aggregation reads. |
lib/mongoid/config.rb |
Nit (1 vote): documents unavailable none_of coverage. Nit (1 vote): Ruby-version wording misstates JRuby timeout behavior. |
lib/mongoid/association/referenced/has_many/proxy.rb |
Bounded association removal. |
lib/mongoid/association/nested/nested_buildable.rb |
Scalar nested ID validation. |
lib/mongoid/association/nested/many.rb |
Moderate (1 vote): absent embedded IDs with _destroy can raise instead of being ignored. |
lib/mongoid/association/depending.rb |
Dependency owner attribute handling. |
lib/config/locales/en.yml |
Timeout messages. |
.evergreen/config/variants.yml.erb |
Moderate (3 votes): disabled matrices remove active application-test variants. |
.evergreen/config.yml |
Moderate (2 votes): disabled matrices remove active application-test variants. |
Review details
Suppressed comments (5)
lib/mongoid/association/nested/many.rb:185
- Because the
association.embedded?branch runs beforedestroyable?, nested attributes containing_destroyfor an id that is absent from an embedded association raiseDocumentNotFoundinstead of being ignored. The referenced-association branch below already implements the documented ignore behavior; test this case and handledestroyable?before the embedded-association not-found branch.
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:151
none_ofis not defined in this 7.6 branch—the new spec notes that it is unavailable atspec/mongoid/criteria/queryable/selectable_where_spec.rb:720—so this documentation promises coverage for an API callers cannot invoke. Remove it from this list or add the API before documenting it.
# +where+, +find_by+, +and+, +or+, +nor+, +not+, +any_of+, and +none_of+.
lib/mongoid/config.rb:136
PER_REGEXP_TIMEOUTis deliberately false for JRuby even when it reports Ruby 3.4, so this 'Ruby 3.2 and later' description is false there: the implementation uses the whole-scopeTimeoutwall-clock fallback. Document the engine capability instead so JRuby users know what the limit covers.
# 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/criteria/queryable/selectable.rb:906
- The guard also misses public selector-building paths that do not use
_mongoid_expand_keys:geo_spatialwrites through__merge__, andmax_distancewrites through__add__/with_strategy. In strict mode, for example,Band.geo_spatial('$where' => js)can still add a server-side JavaScript operator; validate these paths or move the check to a common selector write point.
# It deliberately does not cover the APIs that ask for JavaScript
# outright, such as #js_query and Criteria#for_js: there the developer
# has chosen server-side JavaScript, so there is nothing to guard
# against. The same goes for the low-level Storable methods
# (#add_field_expression, #add_operator_expression), which write to the
# selector directly.
lib/mongoid/criteria/queryable/selectable.rb:820
- This guard now rejects string
wherecriteria while they are being built, somatch.records.where("this.name == null")raisesMongoid::Errors::InvalidQuerybefore the embedded matcher can raise its existingUnsupportedJavascript. The unchanged example atspec/mongoid/criteria_spec.rb:3690-3694will fail; update that expectation/configuration (or preserve the old embedded-specific error) as part of this change.
unless Mongoid.allow_unsafe_query_operators?
raise Errors::InvalidQuery,
"String criteria are not allowed because they compile to the '$where' operator, " \
'which is not allowed in a query expression. Set Mongoid.allow_unsafe_query_operators = true ' \
'to permit all operators.'
- Files reviewed: 39/39 changed files
- Comments generated: 7
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| end | ||
| meth = klass.aliased_associations[segment] || segment | ||
| res.nil? ? document.try(meth) : res | ||
| res.nil? ? read_field_value(document, segment) : res |
Comment on lines
+896
to
+899
| # This is called from #_mongoid_expand_keys rather than from the | ||
| # individual query methods, because that is the one point every query | ||
| # method taking a user-supplied expression passes through on its way to | ||
| # the selector. |
| def bake(regexp) | ||
| return regexp unless PER_REGEXP_TIMEOUT | ||
|
|
||
| ::Regexp.new(regexp.source, regexp.options, timeout: @timeout) |
Comment on lines
+765
to
+769
| # - matrix_name: app-tests-rails-7 | ||
| # matrix_spec: | ||
| # ruby: ["ruby-3.2", "ruby-3.3"] | ||
| # driver: ["current"] | ||
| # mongodb-version: '7.0' |
Comment on lines
+114
to
+118
| # - matrix_name: app-tests-rails-7 | ||
| # matrix_spec: | ||
| # ruby: ["ruby-3.2", "ruby-3.3"] | ||
| # driver: ["current"] | ||
| # mongodb-version: '7.0' |
Comment on lines
+301
to
+303
| ensure | ||
| budget.charge(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) | ||
| end |
| end | ||
|
|
||
| # The key for the time left in the current in-memory regexp budget. | ||
| REGEXP_BUDGET_KEY = 'regexp-budget' |
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 7.6.2 of the
mongoidgem - a Ruby ODM for MongoDB. This is a new patch release in the 7.6.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.
🔴 End-Of-Life Notice
After December 31, 2026, this 7.x series will no longer be supported. It will no longer receive any maintenance or security updates. If you are still running on Mongoid 7.x or earlier, please upgrade.
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) and inspects nested expressions such as{'$expr' => {'$function' => ...}}in full.Other Bug Fixes