Skip to content
Merged
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
4 changes: 4 additions & 0 deletions lib/mongoid/clients/factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ def options(opts)
[MONGOID_WRAPPING_LIBRARY] + options[:wrapping_libraries]
else
[MONGOID_WRAPPING_LIBRARY]
end.tap do |wrap|
if defined?(::Rails) && ::Rails.respond_to?(:version)
wrap << { name: 'Rails', version: ::Rails.version }
end
end
options[:wrapping_libraries] = wrap_lib
end
Expand Down
28 changes: 27 additions & 1 deletion lib/mongoid/extensions/hash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,28 @@ def resizable?
true
end

ALLOWED_TO_CRITERIA_METHODS = %i[
all all_in all_of and any_in any_of asc ascending
batch_size between
collation comment cursor_type
desc descending
elem_match eq exists extras
geo_spatial group gt gte
hint
in includes
limit lt lte
max_distance max_scan max_time_ms merge mod
ne near near_sphere nin no_timeout none none_of nor not not_in
offset only or order order_by
project
raw read reorder
scoped skip slice snapshot
text_search type
unscoped unwind
where with_size with_type without
].freeze
private_constant :ALLOWED_TO_CRITERIA_METHODS

# Convert this hash to a criteria. Will iterate over each keys in the
# hash which must correspond to method on a criteria object. The hash
# must also include a "klass" key.
Expand All @@ -174,7 +196,11 @@ def resizable?
def to_criteria
criteria = Criteria.new(delete(:klass) || delete("klass"))
each_pair do |method, args|
criteria = criteria.__send__(method, args)
method_sym = method.to_sym
unless ALLOWED_TO_CRITERIA_METHODS.include?(method_sym)
raise ArgumentError, "Method '#{method}' is not allowed in to_criteria"
end
criteria = criteria.public_send(method_sym, args)
end
criteria
end
Expand Down
2 changes: 1 addition & 1 deletion lib/mongoid/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Mongoid
VERSION = "7.5.4"
VERSION = "7.6.1"
end
31 changes: 31 additions & 0 deletions spec/mongoid/clients/factory_spec.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# frozen_string_literal: true
# rubocop:todo all

require "spec_helper"

Expand Down Expand Up @@ -29,6 +30,34 @@
end
end

shared_examples_for 'includes rails wrapping library' do
context 'when Rails is available' do
around do |example|
rails_was_defined = defined?(::Rails)

if !rails_was_defined || !::Rails.respond_to?(:version)
module ::Rails
def self.version
'6.1.0'
end
end
end

example.run

if !rails_was_defined
Object.send(:remove_const, :Rails) if defined?(::Rails)
end
end

it 'adds Rails as another wrapping library' do
expect(client.options[:wrapping_libraries]).to include(
{'name' => 'Rails', 'version' => '6.1.0'},
)
end
end
end

describe ".create" do

context "when provided a name" do
Expand Down Expand Up @@ -116,6 +145,8 @@
]
end
end

it_behaves_like 'includes rails wrapping library'
end
end

Expand Down
236 changes: 236 additions & 0 deletions spec/mongoid/extensions/hash_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -467,4 +467,240 @@

it_behaves_like 'unsatisfiable criteria method'
end

describe '#to_criteria' do
subject(:criteria) { hash.to_criteria }

context 'when klass is specified' do
let(:hash) do
{ klass: Band, where: { name: 'Songs Ohia' } }
end

it 'returns a criteria' do
expect(criteria).to be_a(Mongoid::Criteria)
end

it 'sets the klass' do
expect(criteria.klass).to eq(Band)
end

it 'sets the selector' do
expect(criteria.selector).to eq({ 'name' => 'Songs Ohia' })
end
end

context 'when klass is missing' do
let(:hash) do
{ where: { name: 'Songs Ohia' } }
end

it 'returns a criteria' do
expect(criteria).to be_a(Mongoid::Criteria)
end

it 'has klass nil' do
expect(criteria.klass).to be_nil
end

it 'sets the selector' do
expect(criteria.selector).to eq({ 'name' => 'Songs Ohia' })
end
end

context 'with allowed methods' do
context 'when using multiple query methods' do
let(:hash) do
{
klass: Band,
where: { active: true },
limit: 10,
skip: 5,
order_by: { name: 1 }
}
end

it 'applies all methods successfully' do
expect(criteria.selector).to eq({ 'active' => true })
expect(criteria.options[:limit]).to eq(10)
expect(criteria.options[:skip]).to eq(5)
expect(criteria.options[:sort]).to eq({ 'name' => 1 })
end
end

context 'when using query selector methods' do
let(:hash) do
{
klass: Band,
gt: { members: 2 },
in: { genre: ['rock', 'metal'] }
}
end

it 'applies selector methods' do
expect(criteria.selector['members']).to eq({ '$gt' => 2 })
expect(criteria.selector['genre']).to eq({ '$in' => ['rock', 'metal'] })
end
end

context 'when using aggregation methods' do
let(:hash) do
{
klass: Band,
project: { name: 1, members: 1 }
}
end

it 'applies aggregation methods' do
expect { criteria }.not_to raise_error
end
end
end

context 'with disallowed methods' do
context 'when attempting to call create' do
let(:hash) do
{ klass: Band, create: { name: 'Malicious' } }
end

it 'raises ArgumentError' do
expect { criteria }.to raise_error(ArgumentError, "Method 'create' is not allowed in to_criteria")
end
end

context 'when attempting to call create!' do
let(:hash) do
{ klass: Band, 'create!': { name: 'Malicious' } }
end

it 'raises ArgumentError' do
expect { criteria }.to raise_error(ArgumentError, "Method 'create!' is not allowed in to_criteria")
end
end

context 'when attempting to call build' do
let(:hash) do
{ klass: Band, build: { name: 'Malicious' } }
end

it 'raises ArgumentError' do
expect { criteria }.to raise_error(ArgumentError, "Method 'build' is not allowed in to_criteria")
end
end

context 'when attempting to call find' do
let(:hash) do
{ klass: Band, find: 'some_id' }
end

it 'raises ArgumentError' do
expect { criteria }.to raise_error(ArgumentError, "Method 'find' is not allowed in to_criteria")
end
end

context 'when attempting to call execute_or_raise' do
let(:hash) do
{ klass: Band, execute_or_raise: ['id1', 'id2'] }
end

it 'raises ArgumentError' do
expect { criteria }.to raise_error(ArgumentError, "Method 'execute_or_raise' is not allowed in to_criteria")
end
end

context 'when attempting to call new' do
let(:hash) do
{ klass: Band, new: { name: 'Test' } }
end

it 'raises ArgumentError' do
expect { criteria }.to raise_error(ArgumentError, "Method 'new' is not allowed in to_criteria")
end
end

context 'when allowed method is combined with disallowed method' do
let(:hash) do
{
klass: Band,
where: { active: true },
create: { name: 'Malicious' }
}
end

it 'raises ArgumentError before executing any methods' do
expect { criteria }.to raise_error(ArgumentError, "Method 'create' is not allowed in to_criteria")
end
end
end

context 'security validation' do
# This test ensures that ALL public methods not in the allowlist are blocked
it 'blocks all dangerous public methods' do
dangerous_methods = %i[
build create create! new
find find_or_create_by find_or_create_by! find_or_initialize_by
first_or_create first_or_create! first_or_initialize
execute_or_raise multiple_from_db for_ids
documents= inclusions= scoping_options=
initialize freeze as_json
]

dangerous_methods.each do |method|
hash = { klass: Band, method => 'arg' }
expect { hash.to_criteria }.to raise_error(
ArgumentError,
"Method '#{method}' is not allowed in to_criteria"
), "Expected method '#{method}' to be blocked but it was allowed"
end
end

it 'blocks dangerous inherited methods from Object' do
# Critical security test: block send, instance_eval, etc.
inherited_dangerous = %i[
send __send__ instance_eval instance_exec
instance_variable_set method
]

inherited_dangerous.each do |method|
hash = { klass: Band, method => 'arg' }
expect { hash.to_criteria }.to raise_error(
ArgumentError,
"Method '#{method}' is not allowed in to_criteria"
), "Expected inherited method '#{method}' to be blocked"
end
end

it 'blocks Enumerable execution methods' do
# to_criteria should build queries, not execute them
enumerable_methods = %i[each map select count sum]

enumerable_methods.each do |method|
hash = { klass: Band, method => 'arg' }
expect { hash.to_criteria }.to raise_error(
ArgumentError,
"Method '#{method}' is not allowed in to_criteria"
), "Expected Enumerable method '#{method}' to be blocked"
end
end

it 'allows all whitelisted methods' do
# Sample of allowed methods from each category
allowed_sample = {
where: { name: 'Test' }, # Query selector
limit: 10, # Query option
skip: 5, # Query option
gt: { age: 18 }, # Query selector
in: { status: ['active'] }, # Query selector
ascending: :name, # Sorting
includes: :notes, # Eager loading
merge: { klass: Band }, # Merge
}

allowed_sample.each do |method, args|
hash = { klass: Band, method => args }
expect { hash.to_criteria }.not_to raise_error,
"Expected method '#{method}' to be allowed but it was blocked"
end
end
end
end
end
Loading