Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@ gem 'i18n', *i18n_versions

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ windows jruby ]

gem 'ostruct' if RUBY_VERSION >= '4.0'
9 changes: 9 additions & 0 deletions lib/mongoid/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,15 @@ def validate_isolation_level!(level)
# See https://jira.mongodb.org/browse/MONGOID-5892 for more details.
option :serializable_hash_with_legacy_only, default: true

# When true (default), all top-level query operators are passed through
# to MongoDB without restriction when using +where+/+find_by+. Set to
# false to enable a strict allowlist that rejects operators like +$where+
# and +$function+, which can execute arbitrary JavaScript when user-supplied
# input reaches the query builder.
#
# See https://jira.mongodb.org/browse/MONGOID-5939 for details.
option :allow_unsafe_query_operators, default: true

# Returns the Config singleton, for use in the configure DSL.
#
# @return [ self ] The Config singleton.
Expand Down
13 changes: 12 additions & 1 deletion lib/mongoid/criteria/queryable/selectable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,13 @@ def where(*criteria)
#
# @return [ Selectable ] The cloned selectable.
# @api private
# Operators permitted in a query expression without opt-in.
# Excludes $where (JS execution) and other operators not needed for
# ordinary application queries.
ALLOWED_QUERY_OPERATORS = %w[
$and $or $nor $not $text $comment $expr $jsonSchema $alwaysFalse $alwaysTrue
].freeze

def expr_query(criterion)
raise ArgumentError, 'Criterion cannot be nil here' if criterion.nil?
unless criterion.is_a?(Hash)
Expand All @@ -803,7 +810,11 @@ def expr_query(criterion)
normalized.each do |field, value|
field_s = field.to_s
if field_s.start_with?('$')
# Query expression-level operator, like $and or $where
unless Mongoid.allow_unsafe_query_operators? || ALLOWED_QUERY_OPERATORS.include?(field_s)
raise Errors::InvalidQuery,
"Operator '#{field_s}' is not allowed in a query expression. " \
"Set Mongoid.allow_unsafe_query_operators = true to permit all operators."
end
query.add_operator_expression(field_s, value)
else
query.add_field_expression(field, value)
Expand Down
1 change: 1 addition & 0 deletions spec/mongoid/criteria/queryable/selectable_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2163,4 +2163,5 @@ def localized?
end
end
end

end
63 changes: 63 additions & 0 deletions spec/mongoid/criteria/queryable/selectable_where_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -550,4 +550,67 @@ def self.evolve(object)
end
end
end

describe "top-level operator injection guard" do
context "when allow_unsafe_query_operators is true (default)" do
context "when passing $where" do
it "does not raise" do
expect {
query.where('$where' => 'this.name == "admin"')
}.not_to raise_error
end
end

context "when passing $function" do
it "does not raise" do
expect {
query.where('$function' => { body: 'function() { return true; }', args: [], lang: 'js' })
}.not_to raise_error
end
end
end

context "when allow_unsafe_query_operators is false" do
config_override :allow_unsafe_query_operators, false

context "when passing $where" do
it "raises InvalidQuery" do
expect {
query.where('$where' => 'this.name == "admin"')
}.to raise_error(Mongoid::Errors::InvalidQuery, /\$where/)
end
end

context "when passing $function" do
it "raises InvalidQuery" do
expect {
query.where('$function' => { body: 'function() { return true; }', args: [], lang: 'js' })
}.to raise_error(Mongoid::Errors::InvalidQuery, /\$function/)
end
end

context "when the error mentions allow_unsafe_query_operators" do
it "includes the config opt-in in the message" do
expect {
query.where('$where' => 'true')
}.to raise_error(Mongoid::Errors::InvalidQuery, /allow_unsafe_query_operators/)
end
end

%w[$and $or $nor $not $text $comment $expr $jsonSchema $alwaysFalse $alwaysTrue].each do |op|
context "when passing #{op} (allowlisted)" do
it "does not raise" do
value = case op
when '$and', '$or', '$nor', '$not' then [{ 'x' => 1 }]
when '$text' then { '$search' => 'hi' }
when '$expr' then { '$gt' => ['$a', 1] }
when '$jsonSchema' then { 'required' => ['x'] }
else true
end
expect { query.where(op => value) }.not_to raise_error
end
end
end
end
end
end
Loading