mongoid 9.0.9 → 9.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5dca0df68ed7086b6c641b1021e556812e23872d5bd52a9777093e2d8cf1e64
4
- data.tar.gz: 24630d19104369a922b3a5a152012ae8eba3719576ef3c9e5f97304b6896a1c4
3
+ metadata.gz: fefd49681b6f78b222f201fbaea83af9f1599464034e52a73117c0ae94e201f2
4
+ data.tar.gz: d86f966109921d2a0f1a4ce0b78d81e0d722123c1d420f0f695385ca43687626
5
5
  SHA512:
6
- metadata.gz: 03013a0c28cda90cfc3b99c26d22e7672e8ce1c6449e599d7ae39a42d19ba742f37dea7510411ca004c7d5d5f25af80e9230bac63fef8ce43515711d7f669343
7
- data.tar.gz: 8bd5b0c4faa4f5f16fcc56ff612bae5a14fd40fa01513fda545469d225c6cd4909022c066719950147dc116dc1a7bdb79b69d48ad40454aabdb45481994dd69b
6
+ metadata.gz: '09799846ca2a44c7dc4d7aff76fba0f9fdcbe38c5bfb001dccd08eb467800f8fac675cecfa06be94eb76ca35f6f1b57b64557ed171e9c0040bcbe0a701d7827f'
7
+ data.tar.gz: bc0a741e2e26a84e84238f1461a26a29ae13584d9bd050dffc1ccd04f70e4e6a7fa63b20e7dcda8137ce3c37d383a5ae916b0d72ff950d4f330e66e0635ea89c
@@ -104,7 +104,8 @@ module Mongoid
104
104
  # @api private
105
105
  def process_raw_attribute(name, raw, field)
106
106
  value = field ? field.demongoize(raw) : raw
107
- attribute_will_change!(name) if value.resizable?
107
+ is_relation = relations.key?(name)
108
+ attribute_will_change!(name) if value.resizable? && !is_relation
108
109
  value
109
110
  end
110
111
 
@@ -131,6 +131,10 @@ module Mongoid
131
131
  [MONGOID_WRAPPING_LIBRARY] + options[:wrapping_libraries]
132
132
  else
133
133
  [MONGOID_WRAPPING_LIBRARY]
134
+ end.tap do |wrap|
135
+ if defined?(::Rails) && ::Rails.respond_to?(:version)
136
+ wrap << { name: 'Rails', version: ::Rails.version }
137
+ end
134
138
  end
135
139
  options[:wrapping_libraries] = wrap_lib
136
140
  end
@@ -41,24 +41,57 @@ module Mongoid
41
41
  include Clients::Sessions
42
42
  include Options
43
43
 
44
+ # Allowed methods for from_hash to prevent arbitrary method execution.
45
+ # Only query-building methods are allowed, not execution or modification methods.
46
+ ALLOWED_FROM_HASH_METHODS = %i[
47
+ all all_in all_of and any_in any_of asc ascending
48
+ batch_size between
49
+ collation comment cursor_type
50
+ desc descending
51
+ elem_match eq exists extras
52
+ geo_spatial group gt gte
53
+ hint
54
+ in includes
55
+ limit lt lte
56
+ max_distance max_scan max_time_ms merge mod
57
+ ne near near_sphere nin no_timeout none none_of nor not not_in
58
+ offset only or order order_by
59
+ project
60
+ raw read reorder
61
+ scoped skip slice snapshot
62
+ text_search type
63
+ unscoped unwind
64
+ where with_size with_type without
65
+ ].freeze
66
+
44
67
  class << self
45
68
  # Convert the given hash to a criteria. Will iterate over each keys in the
46
- # hash which must correspond to method on a criteria object. The hash
47
- # must also include a "klass" key.
69
+ # hash which must correspond to an allowed method on a criteria object. The hash
70
+ # can include a "klass" key that specifies the model class for the criteria.
48
71
  #
49
72
  # @example Convert the hash to a criteria.
50
73
  # Criteria.from_hash({ klass: Band, where: { name: "Depeche Mode" })
51
74
  #
75
+ # @deprecated This method is deprecated and will
76
+ # be removed in a future release.
77
+ #
52
78
  # @param [ Hash ] hash The hash to convert.
53
79
  #
54
80
  # @return [ Criteria ] The criteria.
81
+ #
82
+ # @raise [ ArgumentError ] If a method is not allowed in from_hash.
55
83
  def from_hash(hash)
56
84
  criteria = Criteria.new(hash.delete(:klass) || hash.delete('klass'))
57
85
  hash.each_pair do |method, args|
58
- criteria = criteria.__send__(method, args)
86
+ method_sym = method.to_sym
87
+ unless ALLOWED_FROM_HASH_METHODS.include?(method_sym)
88
+ raise ArgumentError, "Method '#{method}' is not allowed in from_hash"
89
+ end
90
+ criteria = criteria.public_send(method_sym, args)
59
91
  end
60
92
  criteria
61
93
  end
94
+ Mongoid.deprecate(self, :from_hash)
62
95
  end
63
96
 
64
97
  # Static array used to check with method missing - we only need to ever
@@ -246,7 +279,8 @@ module Mongoid
246
279
  # criteria.merge(other_criteria)
247
280
  #
248
281
  # @example Merge the criteria with a hash. The hash must contain a klass
249
- # key and the key/value pairs correspond to method names/args.
282
+ # key that specifies the model class for the criteria and the key/value
283
+ # pairs correspond to method names/args.
250
284
  #
251
285
  # criteria.merge({
252
286
  # klass: Band,
@@ -254,7 +288,7 @@ module Mongoid
254
288
  # order_by: { name: 1 }
255
289
  # })
256
290
  #
257
- # @param [ Criteria ] other The other criterion to merge with.
291
+ # @param [ Criteria | Hash ] other The other criterion to merge with.
258
292
  #
259
293
  # @return [ Criteria ] A cloned self.
260
294
  def merge(other)
@@ -5,5 +5,5 @@ module Mongoid
5
5
  #
6
6
  # Note that this file is automatically updated via `rake candidate:create`.
7
7
  # Manual changes to this file will be overwritten by that rake task.
8
- VERSION = '9.0.9'
8
+ VERSION = '9.0.10'
9
9
  end
@@ -2722,7 +2722,23 @@ describe Mongoid::Attributes do
2722
2722
  end
2723
2723
  end
2724
2724
 
2725
- context "when modifiying a set referenced with the [] notation" do
2725
+ context "when accessing an embedded document with the attribute accessor" do
2726
+ let(:band) { Band.create! }
2727
+
2728
+ before do
2729
+ Band.where(id: band.id).update_all({
2730
+ :$push => {records: { _id: BSON::ObjectId.new }}
2731
+ })
2732
+ end
2733
+
2734
+ it "does not throw a conflicting update error" do
2735
+ b1 = Band.find(band.id)
2736
+ b1[:records].is_a?(Array).should be true
2737
+ expect { b1.save! }.not_to raise_error
2738
+ end
2739
+ end
2740
+
2741
+ context "when modifying a set referenced with the [] notation" do
2726
2742
  let(:catalog) { Catalog.create!(set_field: [ 1 ].to_set) }
2727
2743
 
2728
2744
  before do
@@ -30,6 +30,34 @@ describe Mongoid::Clients::Factory do
30
30
  end
31
31
  end
32
32
 
33
+ shared_examples_for 'includes rails wrapping library' do
34
+ context 'when Rails is available' do
35
+ around do |example|
36
+ rails_was_defined = defined?(::Rails)
37
+
38
+ if !rails_was_defined
39
+ module ::Rails
40
+ def self.version
41
+ '6.1.0'
42
+ end
43
+ end
44
+ end
45
+
46
+ example.run
47
+
48
+ if !rails_was_defined
49
+ Object.send(:remove_const, :Rails) if defined?(::Rails)
50
+ end
51
+ end
52
+
53
+ it 'adds Rails as another wrapping library' do
54
+ expect(client.options[:wrapping_libraries]).to include(
55
+ {'name' => 'Rails', 'version' => '6.1.0'},
56
+ )
57
+ end
58
+ end
59
+ end
60
+
33
61
  describe ".create" do
34
62
 
35
63
  context "when provided a name" do
@@ -89,6 +117,8 @@ describe Mongoid::Clients::Factory do
89
117
  Mongoid::Clients::Factory::MONGOID_WRAPPING_LIBRARY)]
90
118
  end
91
119
 
120
+ it_behaves_like 'includes rails wrapping library'
121
+
92
122
  context 'when configuration specifies a wrapping library' do
93
123
 
94
124
  let(:config) do
@@ -110,6 +140,8 @@ describe Mongoid::Clients::Factory do
110
140
  {'name' => 'Foo'},
111
141
  ]
112
142
  end
143
+
144
+ it_behaves_like 'includes rails wrapping library'
113
145
  end
114
146
  end
115
147
 
@@ -3250,5 +3250,201 @@ describe Mongoid::Criteria do
3250
3250
  expect(criteria.selector).to eq({ 'name' => 'Songs Ohia' })
3251
3251
  end
3252
3252
  end
3253
+
3254
+ context 'with allowed methods' do
3255
+ context 'when using multiple query methods' do
3256
+ let(:hash) do
3257
+ {
3258
+ klass: Band,
3259
+ where: { active: true },
3260
+ limit: 10,
3261
+ skip: 5,
3262
+ order_by: { name: 1 }
3263
+ }
3264
+ end
3265
+
3266
+ it 'applies all methods successfully' do
3267
+ expect(criteria.selector).to eq({ 'active' => true })
3268
+ expect(criteria.options[:limit]).to eq(10)
3269
+ expect(criteria.options[:skip]).to eq(5)
3270
+ expect(criteria.options[:sort]).to eq({ 'name' => 1 })
3271
+ end
3272
+ end
3273
+
3274
+ context 'when using query selector methods' do
3275
+ let(:hash) do
3276
+ {
3277
+ klass: Band,
3278
+ gt: { members: 2 },
3279
+ in: { genre: ['rock', 'metal'] }
3280
+ }
3281
+ end
3282
+
3283
+ it 'applies selector methods' do
3284
+ expect(criteria.selector['members']).to eq({ '$gt' => 2 })
3285
+ expect(criteria.selector['genre']).to eq({ '$in' => ['rock', 'metal'] })
3286
+ end
3287
+ end
3288
+
3289
+ context 'when using aggregation methods' do
3290
+ let(:hash) do
3291
+ {
3292
+ klass: Band,
3293
+ project: { name: 1, members: 1 }
3294
+ }
3295
+ end
3296
+
3297
+ it 'applies aggregation methods' do
3298
+ expect { criteria }.not_to raise_error
3299
+ end
3300
+ end
3301
+ end
3302
+
3303
+ context 'with disallowed methods' do
3304
+ context 'when attempting to call create' do
3305
+ let(:hash) do
3306
+ { klass: Band, create: { name: 'Malicious' } }
3307
+ end
3308
+
3309
+ it 'raises ArgumentError' do
3310
+ expect { criteria }.to raise_error(ArgumentError, "Method 'create' is not allowed in from_hash")
3311
+ end
3312
+ end
3313
+
3314
+ context 'when attempting to call create!' do
3315
+ let(:hash) do
3316
+ { klass: Band, 'create!': { name: 'Malicious' } }
3317
+ end
3318
+
3319
+ it 'raises ArgumentError' do
3320
+ expect { criteria }.to raise_error(ArgumentError, "Method 'create!' is not allowed in from_hash")
3321
+ end
3322
+ end
3323
+
3324
+ context 'when attempting to call build' do
3325
+ let(:hash) do
3326
+ { klass: Band, build: { name: 'Malicious' } }
3327
+ end
3328
+
3329
+ it 'raises ArgumentError' do
3330
+ expect { criteria }.to raise_error(ArgumentError, "Method 'build' is not allowed in from_hash")
3331
+ end
3332
+ end
3333
+
3334
+ context 'when attempting to call find' do
3335
+ let(:hash) do
3336
+ { klass: Band, find: 'some_id' }
3337
+ end
3338
+
3339
+ it 'raises ArgumentError' do
3340
+ expect { criteria }.to raise_error(ArgumentError, "Method 'find' is not allowed in from_hash")
3341
+ end
3342
+ end
3343
+
3344
+ context 'when attempting to call execute_or_raise' do
3345
+ let(:hash) do
3346
+ { klass: Band, execute_or_raise: ['id1', 'id2'] }
3347
+ end
3348
+
3349
+ it 'raises ArgumentError' do
3350
+ expect { criteria }.to raise_error(ArgumentError, "Method 'execute_or_raise' is not allowed in from_hash")
3351
+ end
3352
+ end
3353
+
3354
+ context 'when attempting to call new' do
3355
+ let(:hash) do
3356
+ { klass: Band, new: { name: 'Test' } }
3357
+ end
3358
+
3359
+ it 'raises ArgumentError' do
3360
+ expect { criteria }.to raise_error(ArgumentError, "Method 'new' is not allowed in from_hash")
3361
+ end
3362
+ end
3363
+
3364
+ context 'when allowed method is combined with disallowed method' do
3365
+ let(:hash) do
3366
+ {
3367
+ klass: Band,
3368
+ where: { active: true },
3369
+ create: { name: 'Malicious' }
3370
+ }
3371
+ end
3372
+
3373
+ it 'raises ArgumentError before executing any methods' do
3374
+ expect { criteria }.to raise_error(ArgumentError, "Method 'create' is not allowed in from_hash")
3375
+ end
3376
+ end
3377
+ end
3378
+
3379
+ context 'security validation' do
3380
+ # This test ensures that ALL public methods not in the allowlist are blocked
3381
+ it 'blocks all dangerous public methods' do
3382
+ dangerous_methods = %i[
3383
+ build create create! new
3384
+ find find_or_create_by find_or_create_by! find_or_initialize_by
3385
+ first_or_create first_or_create! first_or_initialize
3386
+ execute_or_raise multiple_from_db for_ids
3387
+ documents= inclusions= scoping_options=
3388
+ initialize freeze as_json
3389
+ ]
3390
+
3391
+ dangerous_methods.each do |method|
3392
+ hash = { klass: Band, method => 'arg' }
3393
+ expect { described_class.from_hash(hash) }.to raise_error(
3394
+ ArgumentError,
3395
+ "Method '#{method}' is not allowed in from_hash"
3396
+ ), "Expected method '#{method}' to be blocked but it was allowed"
3397
+ end
3398
+ end
3399
+
3400
+ it 'blocks dangerous inherited methods from Object' do
3401
+ # Critical security test: block send, instance_eval, etc.
3402
+ inherited_dangerous = %i[
3403
+ send __send__ instance_eval instance_exec
3404
+ instance_variable_set method
3405
+ ]
3406
+
3407
+ inherited_dangerous.each do |method|
3408
+ hash = { klass: Band, method => 'arg' }
3409
+ expect { described_class.from_hash(hash) }.to raise_error(
3410
+ ArgumentError,
3411
+ "Method '#{method}' is not allowed in from_hash"
3412
+ ), "Expected inherited method '#{method}' to be blocked"
3413
+ end
3414
+ end
3415
+
3416
+ it 'blocks Enumerable execution methods' do
3417
+ # from_hash should build queries, not execute them
3418
+ enumerable_methods = %i[each map select count sum]
3419
+
3420
+ enumerable_methods.each do |method|
3421
+ hash = { klass: Band, method => 'arg' }
3422
+ expect { described_class.from_hash(hash) }.to raise_error(
3423
+ ArgumentError,
3424
+ "Method '#{method}' is not allowed in from_hash"
3425
+ ), "Expected Enumerable method '#{method}' to be blocked"
3426
+ end
3427
+ end
3428
+
3429
+ it 'allows all whitelisted methods' do
3430
+ # Sample of allowed methods from each category
3431
+ allowed_sample = {
3432
+ where: { name: 'Test' }, # Query selector
3433
+ limit: 10, # Query option
3434
+ skip: 5, # Query option
3435
+ gt: { age: 18 }, # Query selector
3436
+ in: { status: ['active'] }, # Query selector
3437
+ ascending: :name, # Sorting
3438
+ includes: :notes, # Eager loading
3439
+ merge: { klass: Band }, # Merge
3440
+ }
3441
+
3442
+ allowed_sample.each do |method, args|
3443
+ hash = { klass: Band, method => args }
3444
+ expect { described_class.from_hash(hash) }.not_to raise_error,
3445
+ "Expected method '#{method}' to be allowed but it was blocked"
3446
+ end
3447
+ end
3448
+ end
3253
3449
  end
3254
3450
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongoid
3
3
  version: !ruby/object:Gem::Version
4
- version: 9.0.9
4
+ version: 9.0.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - The MongoDB Ruby Team
@@ -1231,7 +1231,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
1231
1231
  - !ruby/object:Gem::Version
1232
1232
  version: 1.3.6
1233
1233
  requirements: []
1234
- rubygems_version: 4.0.2
1234
+ rubygems_version: 4.0.4
1235
1235
  specification_version: 4
1236
1236
  summary: Elegant Persistence in Ruby for MongoDB.
1237
1237
  test_files: