Skip to content

Release Candidate: 8.1.13 - #6174

Merged
jamis merged 10 commits into
mongodb:8.1-stablefrom
jamis:updates-8.1
Sep 17, 2026
Merged

jamis merged 10 commits into
mongodb:8.1-stablefrom
jamis:updates-8.1

Conversation

@jamis

@jamis jamis commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

The MongoDB Ruby team is pleased to announce version 8.1.13 of the mongoid gem - 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:

gem install -v 8.1.13 mongoid

Or simply add it to your Gemfile:

gem 'mongoid', '8.1.13'

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 (default 5.0 seconds); exceeding the limit raises Mongoid::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 new Mongoid.allow_reparenting_via_nested_attributes option (default false) restores the previous reparenting behavior when set to true.

Reject the string form of where under the query operator guard (MONGOID-5993)

A String passed to #where is sent to MongoDB as a $where expression. This now raises Mongoid::Errors::InvalidQuery when Mongoid.allow_unsafe_query_operators is false (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_operators now defaults to false. When it is false, the $where, $function, and $accumulator operators 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, and elem_match, including chained operator overrides) and inspects nested expressions such as {'$expr' => {'$function' => ...}} in full.

Other Bug Fixes

  • In-memory queries, such as those executed against embedded associations, now read field names from the query as data rather than dispatching them as method calls on the document (MONGOID-5973)

…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.
@jamis
jamis requested a review from Jibola September 17, 2026 19:12
@jamis
jamis requested a review from a team as a code owner September 17, 2026 19:12
Copilot AI lite review requested due to automatic review settings September 17, 2026 19:12
@jamis jamis added the release-candidate The PR represents a potential candidate for a new release label Sep 17, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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_many with a custom primary_key, the association membership query uses base.send(primary_key) (see has_many.rb:113-115), but this error reports parent.id. The new DocumentNotFound params therefore identify the wrong parent key—for example, _id instead of parent.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 _destroy request for an id absent from an embedded many association raises DocumentNotFound instead 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_TIMEOUT is enabled only for MRI, while the code explicitly uses the Timeout fallback 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 Regexp here 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.
@jamis
jamis merged commit 492a97b into mongodb:8.1-stable Sep 17, 2026
46 checks passed
@jamis
jamis deleted the updates-8.1 branch September 17, 2026 22:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-candidate The PR represents a potential candidate for a new release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants