Release Candidate: 9.0.12 - #6173
Merged
Merged
Conversation
…names as method calls Backport to the 9.0 backport branch.
Squashed commit of the following:
commit ba00b73d21ac69426f5446b04f28fa6f19e442a7
Author: Jamis Buck <jamis.buck@mongodb.com>
Date: Mon Sep 7 12:35:11 2026 -0600
MONGOID-5981 Cap the app-test json gem at 2.x for Rails compatibility
json 3.0 removed the quirks_mode and max_nesting keywords that
ActiveSupport::JSON (every 7.x) still passes to JSON.generate and
JSON.parse. The app-test harness generates a Rails app with a fresh
bundle, so on a machine where the newest json is 3.x every generated
app fails to encode any response with ArgumentError: unknown keyword:
quirks_mode. Pin json < 3 in the generated app's Gemfile (adjust_app_gemfile)
so the bundle resolves a 2.x json that ActiveSupport still accepts.
Incidental to MONGOID-5981; needed so the backport's app tests stay green.
commit 801b4e53d5b02d65acaf0a48aa5d3057de631523
Author: Jamis Buck <jamis.buck@mongodb.com>
Date: Mon Sep 7 11:34:42 2026 -0600
MONGOID-5981 Mark open no-budget scopes with a sentinel
RegexpBudget opened a scope with nothing to bound by storing nil under
Threaded::REGEXP_BUDGET_KEY, relying on Threaded.has? treating a present
nil key as present. On the 9.0 backport branch Threaded.has? is
!get(key).nil?, so a nil value reads as absent and a nested scope
re-decided for itself instead of joining the enclosing no-budget scope.
Store a NO_BUDGET sentinel object instead, which any Threaded.has? reads
as present, and map it back to nil in RegexpBudget.current.
commit d9a621bac4670682c4be628eefe7be88c7258f60
Author: Jamis Buck <jamis.buck@mongodb.com>
Date: Mon Sep 7 10:57:34 2026 -0600
MONGOID-5981 Bound regex execution in the in-memory matcher
The id in a nested attributes hash was resolved against the parent's
association, and on a miss fell back to a collection-wide lookup that
also dropped default scopes. The matched document was then mass
assigned and pushed into the caller's association, so a user could
overwrite a document belonging to someone else by sending that
document's id in a nested form field.
MONGOID-5930 added allow_reparenting_via_nested_attributes to close
this, but the branch was reached unconditionally for HABTM
associations, so the flag never protected them.
- Drop the unconditional HABTM test, so HABTM honours the same flag as
has-many and raises DocumentNotFound instead.
- Reject non-scalar ids in the nested builder, so an operator hash
cannot survive conversion into an id query, and a malformed object id
hash raises a Mongoid error rather than an unhandled BSON error.
- Stop setting allow_reparenting_via_nested_attributes back to true in
load_defaults('9.0'). On the 9.0-stable branch this is done by
defaulting the option to false (it previously defaulted to true).
A destroy of a document that is not in the association is now ignored
for every association type, rather than only when the flag is on.
Backport 5993 to 9.0, 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.
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.
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical encryption issues and moderate enforcement gaps must be addressed before approval.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Release candidate 9.0.12 hardening Mongoid’s in-memory queries, encryption handling, nested attributes, and unsafe query protection.
Changes:
- Adds regexp execution limits and safe in-memory field access.
- Improves encryption schema generation and fail-closed persistence checks.
- Restricts unsafe query operators and disables nested-attribute reparenting by default.
- Updates release metadata and related tests.
Review findings:
lib/mongoid/encryptable.rb:55— Critical, 1 vote: Memoized negative results can omit lazily loaded encrypted schemas and allow plaintext writes.lib/mongoid/encryptable.rb:79— Moderate, 1 vote: The guard can callembeds_encrypted?on an incompatible target and raiseNoMethodError.lib/mongoid/matcher/regexp_budget.rb:296— Moderate, 3 votes: The final regexp match can exhaust the budget without raising a timeout.lib/mongoid/persistence_context.rb:230— Critical, 1 vote: Bypassed automatic encryption can pass namespace validation and write plaintext.lib/mongoid/search_indexable.rb:102— Moderate, 3 votes: Search-index polling can run outside the management scope and incorrectly raiseNoEncryptionSchema.
File summaries
| File | Description |
|---|---|
spec/support/crypt/models.rb |
Adds encryption test models. |
spec/mongoid/tasks/database_spec.rb |
Tests collection creation for encrypted models. |
spec/mongoid/matcher/regexp_budget_spec.rb |
Tests regexp budget behavior. |
spec/mongoid/encryptable_spec.rb |
Tests encryption schema requirements. |
spec/mongoid/criteria/queryable/selectable_where_spec.rb |
Tests query operator guarding. |
spec/mongoid/criteria/queryable/selectable_logical_spec.rb |
Updates unsafe string query tests. |
spec/mongoid/criteria_spec.rb |
Updates unsafe string criteria tests. |
spec/mongoid/contextual/memory_spec.rb |
Tests safe in-memory field reads. |
spec/mongoid/contextual/aggregable/memory_spec.rb |
Tests safe in-memory aggregation. |
spec/mongoid/config/encryption_spec.rb |
Tests generated encryption schemas. |
spec/mongoid/config/defaults_spec.rb |
Tests updated defaults. |
spec/mongoid/attributes/nested_spec.rb |
Tests nested attribute ID validation. |
spec/mongoid/association/referenced/has_many/proxy_spec.rb |
Tests bounded association removal. |
spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb |
Tests HABTM removal behavior. |
spec/integration/query_operator_guard_spec.rb |
Tests query guard integration. |
spec/integration/matcher_regexp_timeout_spec.rb |
Tests regexp timeout integration. |
spec/integration/matcher_operator_data/regex.yml |
Adds regex array matching cases. |
spec/integration/encryption_spec.rb |
Tests encryption failure modes. |
spec/integration/dots_and_dollars_spec.rb |
Updates unsafe operator expectations. |
spec/integration/associations/has_and_belongs_to_many_spec.rb |
Tests reparenting configuration. |
spec/integration/app_spec.rb |
Pins JSON for Rails compatibility. |
product.yml |
Updates release metadata. |
lib/mongoid/version.rb |
Sets version 9.0.12. |
lib/mongoid/threaded.rb |
Adds collection-management thread state. |
lib/mongoid/tasks/database.rb |
Applies collection-management scopes. |
lib/mongoid/search_indexable.rb |
Scopes search-index operations. |
lib/mongoid/persistence_context.rb |
Validates encryption schemas. |
lib/mongoid/matcher/regexp_budget.rb |
Implements regexp execution budgets. |
lib/mongoid/matcher/regex.rb |
Routes regex matching through the budget. |
lib/mongoid/matcher/eq_impl_with_regexp.rb |
Uses budgeted regex equality matching. |
lib/mongoid/matcher.rb |
Loads regexp budget support. |
lib/mongoid/matchable.rb |
Budgets document matching. |
lib/mongoid/indexable.rb |
Scopes index management. |
lib/mongoid/field_readable.rb |
Safely reads field values. |
lib/mongoid/errors/no_encryption_schema.rb |
Adds an encryption schema error. |
lib/mongoid/errors/in_memory_regexp_timeout.rb |
Adds a regexp timeout error. |
lib/mongoid/errors.rb |
Registers new errors. |
lib/mongoid/encryptable.rb |
Detects encrypted embedded schemas. |
lib/mongoid/criteria/queryable/selectable.rb |
Validates query operators. |
lib/mongoid/criteria/queryable/mergeable.rb |
Applies validation during criteria expansion. |
lib/mongoid/contextual/memory.rb |
Budgets memory queries and uses safe reads. |
lib/mongoid/contextual/aggregable/memory.rb |
Uses safe reads during aggregation. |
lib/mongoid/config/encryption.rb |
Generates recursive encryption schema maps. |
lib/mongoid/config.rb |
Adds configuration options and changes defaults. |
lib/mongoid/collection_configurable.rb |
Exempts collection management from encryption checks. |
lib/mongoid/association/relatable.rb |
Adds tolerant relation resolution. |
lib/mongoid/association/referenced/has_many/proxy.rb |
Bounds regexp-based association removal. |
lib/mongoid/association/nested/nested_buildable.rb |
Rejects non-scalar nested IDs. |
lib/mongoid/association/nested/many.rb |
Resolves nested IDs within associations. |
lib/mongoid/association/depending.rb |
Renames dependency ownership metadata. |
lib/config/locales/en.yml |
Adds new error messages and guidance. |
Review details
Suppressed comments (1)
lib/mongoid/encryptable.rb:82
- The plain-target guard only checks for
encrypted?, then unconditionally callsklass.embeds_encrypted?. A valid non-Mongoid embedded target that happens to define an unrelatedencrypted?method will therefore make schema generation fail withNoMethodError; require the recursive method as well (or check for the Mongoid encryption contract) before descending.
next false unless klass.respond_to?(:encrypted?)
next false if path.include?(klass)
klass.encrypted? || klass.embeds_encrypted?(path + [ klass ])
- Files reviewed: 51/51 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+55
to
+57
| return @requires_encryption_schema if defined?(@requires_encryption_schema) | ||
|
|
||
| @requires_encryption_schema = encrypted? || embeds_encrypted?([ self ]) |
Comment on lines
+230
to
+232
| schema_map = client.options.dig(:auto_encryption_options, :schema_map) | ||
| namespace = "#{client.database.name}.#{collection_name}" | ||
| return if schema_map&.key?(namespace) |
| # engine that can reach this rescue at all has them. | ||
| raise timeout_error(budget, pattern&.timeout || ::Regexp.timeout || budget.limit) | ||
| ensure | ||
| budget.charge(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) |
Comment on lines
+102
to
+104
| Threaded.with_collection_management do | ||
| collection.search_indexes(options) | ||
| 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 9.0.12 of the
mongoidgem - a Ruby ODM for MongoDB. This is a new patch release in the 9.0.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.Fix encryption schema generation for automatic encryption (MONGOID-5984) (MONGOID-5989)
Automatic encryption schema generation no longer loops on models that embed themselves, and it now handles polymorphic
embeds_onerelations and embedded schemas that carry their ownencryptMetadatacorrectly. A model whose collection has no entry in the generated schema is no longer written without encryption: such a write raisesMongoid::Errors::NoEncryptionSchemainstead of storing the field in plaintext.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. TheMongoid.allow_reparenting_via_nested_attributesoption now defaults tofalse; set it totrueto restore the previous reparenting behavior.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