mongoid 8.1.11 → 8.1.13
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 +4 -4
- data/lib/config/locales/en.yml +26 -0
- data/lib/mongoid/association/depending.rb +8 -4
- data/lib/mongoid/association/embedded/batchable.rb +11 -10
- data/lib/mongoid/association/embedded/embeds_many/proxy.rb +61 -0
- data/lib/mongoid/association/nested/many.rb +38 -3
- data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
- data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
- data/lib/mongoid/clients/factory.rb +4 -0
- data/lib/mongoid/config.rb +62 -0
- data/lib/mongoid/contextual/aggregable/memory.rb +5 -2
- data/lib/mongoid/contextual/memory.rb +18 -7
- data/lib/mongoid/criteria/queryable/mergeable.rb +12 -0
- data/lib/mongoid/criteria/queryable/selectable.rb +109 -0
- data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
- data/lib/mongoid/errors.rb +1 -0
- data/lib/mongoid/extensions/hash.rb +27 -1
- data/lib/mongoid/field_readable.rb +70 -0
- data/lib/mongoid/matchable.rb +6 -1
- data/lib/mongoid/matcher/eq_impl_with_regexp.rb +3 -5
- data/lib/mongoid/matcher/regex.rb +8 -9
- data/lib/mongoid/matcher/regexp_budget.rb +389 -0
- data/lib/mongoid/matcher.rb +1 -0
- data/lib/mongoid/threaded.rb +3 -0
- data/lib/mongoid/touchable.rb +1 -1
- data/lib/mongoid/version.rb +1 -1
- data/lib/mongoid/warnings.rb +1 -0
- data/spec/integration/app_spec.rb +8 -0
- data/spec/integration/associations/embeds_many_spec.rb +41 -0
- data/spec/integration/associations/has_and_belongs_to_many_spec.rb +17 -2
- data/spec/integration/dots_and_dollars_spec.rb +12 -2
- data/spec/integration/matcher_operator_data/regex.yml +21 -0
- data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
- data/spec/integration/query_operator_guard_spec.rb +89 -0
- data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
- data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
- data/spec/mongoid/attributes/nested_spec.rb +204 -10
- data/spec/mongoid/clients/factory_spec.rb +30 -0
- data/spec/mongoid/contextual/aggregable/memory_spec.rb +91 -0
- data/spec/mongoid/contextual/memory_spec.rb +177 -0
- data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +2 -0
- data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +235 -0
- data/spec/mongoid/criteria_spec.rb +2 -0
- data/spec/mongoid/extensions/hash_spec.rb +236 -0
- data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
- data/spec/mongoid/touchable_spec.rb +12 -0
- data/spec/shared/CANDIDATE.md +28 -0
- data/spec/shared/lib/mrss/spec_organizer.rb +32 -3
- data/spec/shared/shlib/server.sh +1 -1
- metadata +13 -2
|
@@ -35,6 +35,8 @@ describe Mongoid::Criteria::Queryable::Selectable do
|
|
|
35
35
|
it_behaves_like 'requires a non-nil argument'
|
|
36
36
|
|
|
37
37
|
context "when provided a string" do
|
|
38
|
+
# String criteria compile to $where, which requires the opt-in.
|
|
39
|
+
config_override :allow_unsafe_query_operators, true
|
|
38
40
|
|
|
39
41
|
let(:selection) do
|
|
40
42
|
query.where("this.value = 10")
|
|
@@ -586,4 +588,237 @@ describe Mongoid::Criteria::Queryable::Selectable do
|
|
|
586
588
|
end
|
|
587
589
|
end
|
|
588
590
|
end
|
|
591
|
+
|
|
592
|
+
describe 'top-level operator injection guard' do
|
|
593
|
+
context 'when allow_unsafe_query_operators is true' do
|
|
594
|
+
config_override :allow_unsafe_query_operators, true
|
|
595
|
+
|
|
596
|
+
context 'when passing $where' do
|
|
597
|
+
it 'does not raise' do
|
|
598
|
+
expect do
|
|
599
|
+
query.where('$where' => 'this.name == "admin"')
|
|
600
|
+
end.not_to raise_error
|
|
601
|
+
end
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
context 'when passing a string criterion' do
|
|
605
|
+
it 'does not raise' do
|
|
606
|
+
expect do
|
|
607
|
+
query.where('this.name == "admin"')
|
|
608
|
+
end.not_to raise_error
|
|
609
|
+
end
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
context 'when passing a string criterion to a negated query' do
|
|
613
|
+
it 'does not raise' do
|
|
614
|
+
expect do
|
|
615
|
+
query.not.where('this.name == "admin"')
|
|
616
|
+
end.not_to raise_error
|
|
617
|
+
end
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
context 'when passing $function' do
|
|
621
|
+
it 'does not raise' do
|
|
622
|
+
expect do
|
|
623
|
+
query.where('$function' => { body: 'function() { return true; }', args: [], lang: 'js' })
|
|
624
|
+
end.not_to raise_error
|
|
625
|
+
end
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
context 'when allow_unsafe_query_operators is false' do
|
|
630
|
+
config_override :allow_unsafe_query_operators, false
|
|
631
|
+
|
|
632
|
+
context 'when passing $where' do
|
|
633
|
+
it 'raises InvalidQuery' do
|
|
634
|
+
expect do
|
|
635
|
+
query.where('$where' => 'this.name == "admin"')
|
|
636
|
+
end.to raise_error(Mongoid::Errors::InvalidQuery, /\$where/)
|
|
637
|
+
end
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
context 'when passing $function' do
|
|
641
|
+
it 'raises InvalidQuery' do
|
|
642
|
+
expect do
|
|
643
|
+
query.where('$function' => { body: 'function() { return true; }', args: [], lang: 'js' })
|
|
644
|
+
end.to raise_error(Mongoid::Errors::InvalidQuery, /\$function/)
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
context 'when the error mentions allow_unsafe_query_operators' do
|
|
649
|
+
it 'includes the config opt-in in the message' do
|
|
650
|
+
expect do
|
|
651
|
+
query.where('$where' => 'true')
|
|
652
|
+
end.to raise_error(Mongoid::Errors::InvalidQuery, /allow_unsafe_query_operators/)
|
|
653
|
+
end
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
context 'when passing a string criterion' do
|
|
657
|
+
it 'raises InvalidQuery' do
|
|
658
|
+
expect do
|
|
659
|
+
query.where('this.name == "admin"')
|
|
660
|
+
end.to raise_error(Mongoid::Errors::InvalidQuery, /\$where/)
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
it 'includes the config opt-in in the message' do
|
|
664
|
+
expect do
|
|
665
|
+
query.where('this.name == "admin"')
|
|
666
|
+
end.to raise_error(Mongoid::Errors::InvalidQuery, /allow_unsafe_query_operators/)
|
|
667
|
+
end
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
context 'when passing a string criterion to a negated query' do
|
|
671
|
+
it 'raises InvalidQuery' do
|
|
672
|
+
expect do
|
|
673
|
+
query.not.where('this.name == "admin"')
|
|
674
|
+
end.to raise_error(Mongoid::Errors::InvalidQuery, /\$where/)
|
|
675
|
+
end
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
%w[$and $or $nor $not $text $comment $expr $jsonSchema $alwaysFalse $alwaysTrue].each do |op|
|
|
679
|
+
context "when passing #{op} (allowlisted)" do
|
|
680
|
+
it 'does not raise' do
|
|
681
|
+
value = case op
|
|
682
|
+
when '$and', '$or', '$nor', '$not' then [ { 'x' => 1 } ]
|
|
683
|
+
when '$text' then { '$search' => 'hi' }
|
|
684
|
+
when '$expr' then { '$gt' => [ '$a', 1 ] }
|
|
685
|
+
when '$jsonSchema' then { 'required' => [ 'x' ] }
|
|
686
|
+
else true
|
|
687
|
+
end
|
|
688
|
+
expect { query.where(op => value) }.not_to raise_error
|
|
689
|
+
end
|
|
690
|
+
end
|
|
691
|
+
end
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
describe 'nested operator injection guard' do
|
|
696
|
+
js = 'this.name == "admin"'
|
|
697
|
+
|
|
698
|
+
function_expr = {
|
|
699
|
+
'$function' => { 'body' => 'function() { return true; }', 'args' => [], 'lang' => 'js' }
|
|
700
|
+
}.freeze
|
|
701
|
+
|
|
702
|
+
accumulator_expr = {
|
|
703
|
+
'$accumulator' => {
|
|
704
|
+
'init' => 'function() { return 0; }',
|
|
705
|
+
'accumulate' => 'function() { return 0; }',
|
|
706
|
+
'accumulateArgs' => [],
|
|
707
|
+
'merge' => 'function() { return 0; }',
|
|
708
|
+
'lang' => 'js'
|
|
709
|
+
}
|
|
710
|
+
}.freeze
|
|
711
|
+
|
|
712
|
+
# Every path into the selector that does not route through #expr_query,
|
|
713
|
+
# plus the nested forms that a top-level key check cannot see.
|
|
714
|
+
unsafe_queries = {
|
|
715
|
+
'and with a $where key' => ->(query) { query.and('$where' => js) },
|
|
716
|
+
'or with a $where key' => ->(query) { query.or('$where' => js) },
|
|
717
|
+
'nor with a $where key' => ->(query) { query.nor('$where' => js) },
|
|
718
|
+
'not with a $where key' => ->(query) { query.not('$where' => js) },
|
|
719
|
+
'any_of with a $where key' => ->(query) { query.any_of('$where' => js) },
|
|
720
|
+
'none_of with a $where key' => ->(query) { query.none_of('$where' => js) },
|
|
721
|
+
'elem_match with a nested $where' => ->(query) { query.elem_match(a: { '$where' => js }) },
|
|
722
|
+
'where with $where inside $or' => ->(query) { query.where('$or' => [ { '$where' => js } ]) },
|
|
723
|
+
'where with $function inside $expr' => ->(query) { query.where('$expr' => function_expr) },
|
|
724
|
+
'where with $accumulator inside $expr' => ->(query) { query.where('$expr' => accumulator_expr) },
|
|
725
|
+
'where with $where three levels deep' => lambda { |query|
|
|
726
|
+
query.where('$and' => [ { '$or' => [ { '$where' => js } ] } ])
|
|
727
|
+
},
|
|
728
|
+
'where with a symbol $where key inside $or' => ->(query) { query.where('$or' => [ { :$where => js } ]) }
|
|
729
|
+
}.freeze
|
|
730
|
+
|
|
731
|
+
context 'when allow_unsafe_query_operators is false' do
|
|
732
|
+
config_override :allow_unsafe_query_operators, false
|
|
733
|
+
|
|
734
|
+
unsafe_queries.each do |description, builder|
|
|
735
|
+
context "when querying with #{description}" do
|
|
736
|
+
it 'raises InvalidQuery' do
|
|
737
|
+
expect do
|
|
738
|
+
builder.call(query)
|
|
739
|
+
end.to raise_error(Mongoid::Errors::InvalidQuery, /\$where|\$function|\$accumulator/)
|
|
740
|
+
end
|
|
741
|
+
end
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
it 'includes the config opt-in in the message' do
|
|
745
|
+
expect do
|
|
746
|
+
query.or('$where' => js)
|
|
747
|
+
end.to raise_error(Mongoid::Errors::InvalidQuery, /allow_unsafe_query_operators/)
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
context 'when the nested expression is safe' do
|
|
751
|
+
it 'permits $or with field expressions' do
|
|
752
|
+
expect do
|
|
753
|
+
query.where('$or' => [ { 'a' => 1 }, { 'b' => { '$gt' => 2 } } ])
|
|
754
|
+
end.not_to raise_error
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
it 'permits $expr with aggregation operators' do
|
|
758
|
+
expect do
|
|
759
|
+
query.where('$expr' => { '$gt' => [ '$a', '$b' ] })
|
|
760
|
+
end.not_to raise_error
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
it 'permits a field whose name resembles a javascript operator' do
|
|
764
|
+
expect do
|
|
765
|
+
query.where('where' => js)
|
|
766
|
+
end.not_to raise_error
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
it 'permits a value that resembles a javascript operator' do
|
|
770
|
+
expect do
|
|
771
|
+
query.where('name' => '$where')
|
|
772
|
+
end.not_to raise_error
|
|
773
|
+
end
|
|
774
|
+
end
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
context 'when allow_unsafe_query_operators is true' do
|
|
778
|
+
config_override :allow_unsafe_query_operators, true
|
|
779
|
+
|
|
780
|
+
unsafe_queries.each do |description, builder|
|
|
781
|
+
context "when querying with #{description}" do
|
|
782
|
+
it 'does not raise' do
|
|
783
|
+
expect { builder.call(query) }.not_to raise_error
|
|
784
|
+
end
|
|
785
|
+
end
|
|
786
|
+
end
|
|
787
|
+
end
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
describe 'operator guard on chained override methods' do
|
|
791
|
+
# On this branch overwrite_chained_operators defaults to false, which
|
|
792
|
+
# routes elem_match through the guarded and_with_operator funnel. Force
|
|
793
|
+
# the override path so the guard in Mergeable#__override__ is exercised.
|
|
794
|
+
config_override :overwrite_chained_operators, true
|
|
795
|
+
|
|
796
|
+
let(:js) { 'this.name == "admin"' }
|
|
797
|
+
|
|
798
|
+
context 'when allow_unsafe_query_operators is false' do
|
|
799
|
+
config_override :allow_unsafe_query_operators, false
|
|
800
|
+
|
|
801
|
+
it 'rejects a $where nested in elem_match' do
|
|
802
|
+
expect do
|
|
803
|
+
query.elem_match(a: { '$where' => js })
|
|
804
|
+
end.to raise_error(Mongoid::Errors::InvalidQuery, /executes server-side JavaScript/)
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
it 'permits an ordinary elem_match value' do
|
|
808
|
+
expect do
|
|
809
|
+
query.elem_match(a: { name: 'x' })
|
|
810
|
+
end.not_to raise_error
|
|
811
|
+
end
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
context 'when allow_unsafe_query_operators is true' do
|
|
815
|
+
config_override :allow_unsafe_query_operators, true
|
|
816
|
+
|
|
817
|
+
it 'permits a $where nested in elem_match' do
|
|
818
|
+
expect do
|
|
819
|
+
query.elem_match(a: { '$where' => js })
|
|
820
|
+
end.not_to raise_error
|
|
821
|
+
end
|
|
822
|
+
end
|
|
823
|
+
end
|
|
589
824
|
end
|
|
@@ -2659,6 +2659,8 @@ describe Mongoid::Criteria do
|
|
|
2659
2659
|
end
|
|
2660
2660
|
|
|
2661
2661
|
context "when the criteria is not embedded" do
|
|
2662
|
+
# String criteria compile to $where, which requires the opt-in.
|
|
2663
|
+
config_override :allow_unsafe_query_operators, true
|
|
2662
2664
|
|
|
2663
2665
|
let(:criteria) do
|
|
2664
2666
|
Band.where("this.name == 'Depeche Mode'")
|
|
@@ -497,4 +497,240 @@ describe Mongoid::Extensions::Hash do
|
|
|
497
497
|
|
|
498
498
|
it_behaves_like 'unsatisfiable criteria method'
|
|
499
499
|
end
|
|
500
|
+
|
|
501
|
+
describe '#to_criteria' do
|
|
502
|
+
subject(:criteria) { hash.to_criteria }
|
|
503
|
+
|
|
504
|
+
context 'when klass is specified' do
|
|
505
|
+
let(:hash) do
|
|
506
|
+
{ klass: Band, where: { name: 'Songs Ohia' } }
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
it 'returns a criteria' do
|
|
510
|
+
expect(criteria).to be_a(Mongoid::Criteria)
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
it 'sets the klass' do
|
|
514
|
+
expect(criteria.klass).to eq(Band)
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
it 'sets the selector' do
|
|
518
|
+
expect(criteria.selector).to eq({ 'name' => 'Songs Ohia' })
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
context 'when klass is missing' do
|
|
523
|
+
let(:hash) do
|
|
524
|
+
{ where: { name: 'Songs Ohia' } }
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
it 'returns a criteria' do
|
|
528
|
+
expect(criteria).to be_a(Mongoid::Criteria)
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
it 'has klass nil' do
|
|
532
|
+
expect(criteria.klass).to be_nil
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
it 'sets the selector' do
|
|
536
|
+
expect(criteria.selector).to eq({ 'name' => 'Songs Ohia' })
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
context 'with allowed methods' do
|
|
541
|
+
context 'when using multiple query methods' do
|
|
542
|
+
let(:hash) do
|
|
543
|
+
{
|
|
544
|
+
klass: Band,
|
|
545
|
+
where: { active: true },
|
|
546
|
+
limit: 10,
|
|
547
|
+
skip: 5,
|
|
548
|
+
order_by: { name: 1 }
|
|
549
|
+
}
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
it 'applies all methods successfully' do
|
|
553
|
+
expect(criteria.selector).to eq({ 'active' => true })
|
|
554
|
+
expect(criteria.options[:limit]).to eq(10)
|
|
555
|
+
expect(criteria.options[:skip]).to eq(5)
|
|
556
|
+
expect(criteria.options[:sort]).to eq({ 'name' => 1 })
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
context 'when using query selector methods' do
|
|
561
|
+
let(:hash) do
|
|
562
|
+
{
|
|
563
|
+
klass: Band,
|
|
564
|
+
gt: { members: 2 },
|
|
565
|
+
in: { genre: ['rock', 'metal'] }
|
|
566
|
+
}
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
it 'applies selector methods' do
|
|
570
|
+
expect(criteria.selector['members']).to eq({ '$gt' => 2 })
|
|
571
|
+
expect(criteria.selector['genre']).to eq({ '$in' => ['rock', 'metal'] })
|
|
572
|
+
end
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
context 'when using aggregation methods' do
|
|
576
|
+
let(:hash) do
|
|
577
|
+
{
|
|
578
|
+
klass: Band,
|
|
579
|
+
project: { name: 1, members: 1 }
|
|
580
|
+
}
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
it 'applies aggregation methods' do
|
|
584
|
+
expect { criteria }.not_to raise_error
|
|
585
|
+
end
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
context 'with disallowed methods' do
|
|
590
|
+
context 'when attempting to call create' do
|
|
591
|
+
let(:hash) do
|
|
592
|
+
{ klass: Band, create: { name: 'Malicious' } }
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
it 'raises ArgumentError' do
|
|
596
|
+
expect { criteria }.to raise_error(ArgumentError, "Method 'create' is not allowed in to_criteria")
|
|
597
|
+
end
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
context 'when attempting to call create!' do
|
|
601
|
+
let(:hash) do
|
|
602
|
+
{ klass: Band, 'create!': { name: 'Malicious' } }
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
it 'raises ArgumentError' do
|
|
606
|
+
expect { criteria }.to raise_error(ArgumentError, "Method 'create!' is not allowed in to_criteria")
|
|
607
|
+
end
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
context 'when attempting to call build' do
|
|
611
|
+
let(:hash) do
|
|
612
|
+
{ klass: Band, build: { name: 'Malicious' } }
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
it 'raises ArgumentError' do
|
|
616
|
+
expect { criteria }.to raise_error(ArgumentError, "Method 'build' is not allowed in to_criteria")
|
|
617
|
+
end
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
context 'when attempting to call find' do
|
|
621
|
+
let(:hash) do
|
|
622
|
+
{ klass: Band, find: 'some_id' }
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
it 'raises ArgumentError' do
|
|
626
|
+
expect { criteria }.to raise_error(ArgumentError, "Method 'find' is not allowed in to_criteria")
|
|
627
|
+
end
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
context 'when attempting to call execute_or_raise' do
|
|
631
|
+
let(:hash) do
|
|
632
|
+
{ klass: Band, execute_or_raise: ['id1', 'id2'] }
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
it 'raises ArgumentError' do
|
|
636
|
+
expect { criteria }.to raise_error(ArgumentError, "Method 'execute_or_raise' is not allowed in to_criteria")
|
|
637
|
+
end
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
context 'when attempting to call new' do
|
|
641
|
+
let(:hash) do
|
|
642
|
+
{ klass: Band, new: { name: 'Test' } }
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
it 'raises ArgumentError' do
|
|
646
|
+
expect { criteria }.to raise_error(ArgumentError, "Method 'new' is not allowed in to_criteria")
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
context 'when allowed method is combined with disallowed method' do
|
|
651
|
+
let(:hash) do
|
|
652
|
+
{
|
|
653
|
+
klass: Band,
|
|
654
|
+
where: { active: true },
|
|
655
|
+
create: { name: 'Malicious' }
|
|
656
|
+
}
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
it 'raises ArgumentError before executing any methods' do
|
|
660
|
+
expect { criteria }.to raise_error(ArgumentError, "Method 'create' is not allowed in to_criteria")
|
|
661
|
+
end
|
|
662
|
+
end
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
context 'security validation' do
|
|
666
|
+
# This test ensures that ALL public methods not in the allowlist are blocked
|
|
667
|
+
it 'blocks all dangerous public methods' do
|
|
668
|
+
dangerous_methods = %i[
|
|
669
|
+
build create create! new
|
|
670
|
+
find find_or_create_by find_or_create_by! find_or_initialize_by
|
|
671
|
+
first_or_create first_or_create! first_or_initialize
|
|
672
|
+
execute_or_raise multiple_from_db for_ids
|
|
673
|
+
documents= inclusions= scoping_options=
|
|
674
|
+
initialize freeze as_json
|
|
675
|
+
]
|
|
676
|
+
|
|
677
|
+
dangerous_methods.each do |method|
|
|
678
|
+
hash = { klass: Band, method => 'arg' }
|
|
679
|
+
expect { hash.to_criteria }.to raise_error(
|
|
680
|
+
ArgumentError,
|
|
681
|
+
"Method '#{method}' is not allowed in to_criteria"
|
|
682
|
+
), "Expected method '#{method}' to be blocked but it was allowed"
|
|
683
|
+
end
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
it 'blocks dangerous inherited methods from Object' do
|
|
687
|
+
# Critical security test: block send, instance_eval, etc.
|
|
688
|
+
inherited_dangerous = %i[
|
|
689
|
+
send __send__ instance_eval instance_exec
|
|
690
|
+
instance_variable_set method
|
|
691
|
+
]
|
|
692
|
+
|
|
693
|
+
inherited_dangerous.each do |method|
|
|
694
|
+
hash = { klass: Band, method => 'arg' }
|
|
695
|
+
expect { hash.to_criteria }.to raise_error(
|
|
696
|
+
ArgumentError,
|
|
697
|
+
"Method '#{method}' is not allowed in to_criteria"
|
|
698
|
+
), "Expected inherited method '#{method}' to be blocked"
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
it 'blocks Enumerable execution methods' do
|
|
703
|
+
# to_criteria should build queries, not execute them
|
|
704
|
+
enumerable_methods = %i[each map select count sum]
|
|
705
|
+
|
|
706
|
+
enumerable_methods.each do |method|
|
|
707
|
+
hash = { klass: Band, method => 'arg' }
|
|
708
|
+
expect { hash.to_criteria }.to raise_error(
|
|
709
|
+
ArgumentError,
|
|
710
|
+
"Method '#{method}' is not allowed in to_criteria"
|
|
711
|
+
), "Expected Enumerable method '#{method}' to be blocked"
|
|
712
|
+
end
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
it 'allows all whitelisted methods' do
|
|
716
|
+
# Sample of allowed methods from each category
|
|
717
|
+
allowed_sample = {
|
|
718
|
+
where: { name: 'Test' }, # Query selector
|
|
719
|
+
limit: 10, # Query option
|
|
720
|
+
skip: 5, # Query option
|
|
721
|
+
gt: { age: 18 }, # Query selector
|
|
722
|
+
in: { status: ['active'] }, # Query selector
|
|
723
|
+
ascending: :name, # Sorting
|
|
724
|
+
includes: :notes, # Eager loading
|
|
725
|
+
merge: { klass: Band }, # Merge
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
allowed_sample.each do |method, args|
|
|
729
|
+
hash = { klass: Band, method => args }
|
|
730
|
+
expect { hash.to_criteria }.not_to raise_error,
|
|
731
|
+
"Expected method '#{method}' to be allowed but it was blocked"
|
|
732
|
+
end
|
|
733
|
+
end
|
|
734
|
+
end
|
|
735
|
+
end
|
|
500
736
|
end
|