Skip to content

Commit bb35c66

Browse files
committed
Make validation execute in stages to avoid possible infinite recursion
1 parent 5304237 commit bb35c66

File tree

6 files changed

+165
-58
lines changed

6 files changed

+165
-58
lines changed

juniper/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
- No changes yet
44

5+
# [[0.15.9] 2022-02-02](https://github.com/graphql-rust/juniper/releases/tag/juniper-v0.15.9)
6+
7+
- Fix infinite recursion on malformed queries with nested recursive fragments. *This is a potential denial-of-service attack vector.* Thanks to [@quapka](https://github.com/quapka) for the detailed vulnerability report and reproduction steps.
8+
59
# [[0.15.8] 2022-01-26](https://github.com/graphql-rust/juniper/releases/tag/juniper-v0.15.8)
610

711
- Fix panic on malformed queries with recursive fragments. *This is a potential denial-of-service attack vector.* Thanks to [@quapka](https://github.com/quapka) for the detailed vulnerability report and reproduction steps.

juniper/src/validation/context.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ impl<'a, S: Debug> ValidatorContext<'a, S> {
9797
self.errors.push(RuleError::new(message, locations))
9898
}
9999

100+
pub(crate) fn has_errors(&self) -> bool {
101+
!self.errors.is_empty()
102+
}
103+
100104
#[doc(hidden)]
101105
pub fn into_errors(mut self) -> Vec<RuleError> {
102106
self.errors.sort();

juniper/src/validation/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ pub use self::{
2121

2222
#[cfg(test)]
2323
pub use self::test_harness::{
24-
expect_fails_rule, expect_fails_rule_with_schema, expect_passes_rule,
24+
expect_fails_fn, expect_fails_fn_with_schema, expect_fails_rule, expect_fails_rule_with_schema,
25+
expect_passes_fn, expect_passes_fn_with_schema, expect_passes_rule,
2526
expect_passes_rule_with_schema,
2627
};

juniper/src/validation/rules/mod.rs

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,14 @@ pub fn visit_all_rules<'a, S: Debug>(ctx: &mut ValidatorContext<'a, S>, doc: &'a
3535
where
3636
S: ScalarValue,
3737
{
38-
let mut mv = MultiVisitorNil
38+
// Some validators are depending on the results of other ones.
39+
// For example, validators checking fragments usually rely on the fact that
40+
// they have no cycles (`no_fragment_cycles`), otherwise may stall in an
41+
// infinite recursion. So, we should run validators in stages, moving to the
42+
// next stage only once the previous succeeds. This is better than making
43+
// every single validator being aware of fragments cycles and/or other
44+
// assumptions.
45+
let mut stage1 = MultiVisitorNil
3946
.with(self::arguments_of_correct_type::factory())
4047
.with(self::default_values_of_correct_type::factory())
4148
.with(self::fields_on_correct_type::factory())
@@ -49,7 +56,6 @@ where
4956
.with(self::no_undefined_variables::factory())
5057
.with(self::no_unused_fragments::factory())
5158
.with(self::no_unused_variables::factory())
52-
.with(self::overlapping_fields_can_be_merged::factory())
5359
.with(self::possible_fragment_spreads::factory())
5460
.with(self::provided_non_null_arguments::factory())
5561
.with(self::scalar_leafs::factory())
@@ -60,6 +66,62 @@ where
6066
.with(self::unique_variable_names::factory())
6167
.with(self::variables_are_input_types::factory())
6268
.with(self::variables_in_allowed_position::factory());
69+
visit(&mut stage1, ctx, doc);
70+
if ctx.has_errors() {
71+
return;
72+
}
6373

64-
visit(&mut mv, ctx, doc)
74+
let mut stage2 = MultiVisitorNil.with(self::overlapping_fields_can_be_merged::factory());
75+
visit(&mut stage2, ctx, doc);
76+
}
77+
78+
#[cfg(test)]
79+
mod tests {
80+
use crate::{parser::SourcePosition, DefaultScalarValue};
81+
82+
use crate::validation::{expect_fails_fn, RuleError};
83+
84+
#[test]
85+
fn handles_recursive_fragments() {
86+
expect_fails_fn::<_, DefaultScalarValue>(
87+
super::visit_all_rules,
88+
"fragment f on QueryRoot { ...f }",
89+
&[
90+
RuleError::new(
91+
"Fragment \"f\" is never used",
92+
&[SourcePosition::new(0, 0, 0)],
93+
),
94+
RuleError::new(
95+
"Cannot spread fragment \"f\"",
96+
&[SourcePosition::new(26, 0, 26)],
97+
),
98+
],
99+
);
100+
}
101+
102+
#[test]
103+
fn handles_nested_recursive_fragments() {
104+
expect_fails_fn::<_, DefaultScalarValue>(
105+
super::visit_all_rules,
106+
"fragment f on QueryRoot { a { ...f a { ...f } } }",
107+
&[
108+
RuleError::new(
109+
"Fragment \"f\" is never used",
110+
&[SourcePosition::new(0, 0, 0)],
111+
),
112+
RuleError::new(
113+
r#"Unknown field "a" on type "QueryRoot""#,
114+
&[SourcePosition::new(26, 0, 26)],
115+
),
116+
RuleError::new(
117+
"Cannot spread fragment \"f\"",
118+
&[SourcePosition::new(30, 0, 30)],
119+
),
120+
RuleError::new(
121+
"Cannot spread fragment \"f\"",
122+
&[SourcePosition::new(39, 0, 39)],
123+
),
124+
],
125+
);
126+
}
65127
}

juniper/src/validation/rules/overlapping_fields_can_be_merged.rs

Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ struct PairSet<'a> {
3333
data: HashMap<&'a str, HashMap<&'a str, bool>>,
3434
}
3535

36+
#[derive(Debug)]
3637
struct OrderedMap<K, V> {
3738
data: HashMap<K, V>,
3839
insert_order: Vec<K>,
@@ -172,13 +173,6 @@ impl<'a, S: Debug> OverlappingFieldsCanBeMerged<'a, S> {
172173
);
173174

174175
for frag_name2 in &fragment_names[i + 1..] {
175-
// Prevent infinite fragment recursion. This case is
176-
// caught by fragment validators, but because the validation is
177-
// done in parallel we can't rely on fragments being
178-
// non-recursive here.
179-
if frag_name1 == frag_name2 {
180-
continue;
181-
}
182176
self.collect_conflicts_between_fragments(
183177
&mut conflicts,
184178
frag_name1,
@@ -202,10 +196,8 @@ impl<'a, S: Debug> OverlappingFieldsCanBeMerged<'a, S> {
202196
) where
203197
S: ScalarValue,
204198
{
205-
// Prevent infinite fragment recursion. This case is
206-
// caught by fragment validators, but because the validation is
207-
// done in parallel we can't rely on fragments being
208-
// non-recursive here.
199+
// Early return on fragment recursion, as it makes no sense.
200+
// Fragment recursions are prevented by `no_fragment_cycles` validator.
209201
if fragment_name1 == fragment_name2 {
210202
return;
211203
}
@@ -293,10 +285,8 @@ impl<'a, S: Debug> OverlappingFieldsCanBeMerged<'a, S> {
293285
self.collect_conflicts_between(conflicts, mutually_exclusive, field_map, &field_map2, ctx);
294286

295287
for fragment_name2 in fragment_names2 {
296-
// Prevent infinite fragment recursion. This case is
297-
// caught by fragment validators, but because the validation is
298-
// done in parallel we can't rely on fragments being
299-
// non-recursive here.
288+
// Early return on fragment recursion, as it makes no sense.
289+
// Fragment recursions are prevented by `no_fragment_cycles` validator.
300290
if fragment_name == fragment_name2 {
301291
return;
302292
}
@@ -2279,26 +2269,6 @@ mod tests {
22792269
);
22802270
}
22812271

2282-
#[test]
2283-
fn handles_recursive_fragments() {
2284-
expect_passes_rule_with_schema::<
2285-
_,
2286-
EmptyMutation<()>,
2287-
EmptySubscription<()>,
2288-
_,
2289-
_,
2290-
DefaultScalarValue,
2291-
>(
2292-
QueryRoot,
2293-
EmptyMutation::new(),
2294-
EmptySubscription::new(),
2295-
factory,
2296-
r#"
2297-
fragment f on Query { ...f }
2298-
"#,
2299-
);
2300-
}
2301-
23022272
#[test]
23032273
fn error_message_contains_hint_for_alias_conflict() {
23042274
assert_eq!(

juniper/src/validation/test_harness.rs

Lines changed: 85 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::{
2-
ast::{FromInputValue, InputValue},
2+
ast::{Document, FromInputValue, InputValue},
33
executor::Registry,
44
parser::parse_document_source,
55
schema::{
@@ -812,20 +812,13 @@ where
812812
}
813813
}
814814

815-
pub fn validate<'a, Q, M, Sub, V, F, S>(
816-
r: Q,
817-
m: M,
818-
s: Sub,
819-
q: &'a str,
820-
factory: F,
821-
) -> Vec<RuleError>
815+
pub fn validate<'a, Q, M, Sub, F, S>(r: Q, m: M, s: Sub, q: &'a str, visit_fn: F) -> Vec<RuleError>
822816
where
823817
S: ScalarValue + 'a,
824818
Q: GraphQLType<S, TypeInfo = ()>,
825819
M: GraphQLType<S, TypeInfo = ()>,
826820
Sub: GraphQLType<S, TypeInfo = ()>,
827-
V: Visitor<'a, S> + 'a,
828-
F: Fn() -> V,
821+
F: FnOnce(&mut ValidatorContext<'a, S>, &'a Document<S>),
829822
{
830823
let mut root = RootNode::new_with_scalar_value(r, m, s);
831824

@@ -864,10 +857,7 @@ where
864857
parse_document_source(q, &root.schema).expect(&format!("Parse error on input {:#?}", q));
865858
let mut ctx = ValidatorContext::new(unsafe { ::std::mem::transmute(&root.schema) }, &doc);
866859

867-
let mut mv = MultiVisitorNil.with(factory());
868-
visit(&mut mv, &mut ctx, unsafe {
869-
::std::mem::transmute(doc.as_slice())
870-
});
860+
visit_fn(&mut ctx, unsafe { ::std::mem::transmute(doc.as_slice()) });
871861

872862
ctx.into_errors()
873863
}
@@ -881,6 +871,14 @@ where
881871
expect_passes_rule_with_schema(QueryRoot, MutationRoot, SubscriptionRoot, factory, q);
882872
}
883873

874+
pub fn expect_passes_fn<'a, F, S>(visit_fn: F, q: &'a str)
875+
where
876+
S: ScalarValue + 'a,
877+
F: FnOnce(&mut ValidatorContext<'a, S>, &'a Document<S>),
878+
{
879+
expect_passes_fn_with_schema(QueryRoot, MutationRoot, SubscriptionRoot, visit_fn, q);
880+
}
881+
884882
pub fn expect_passes_rule_with_schema<'a, Q, M, Sub, V, F, S>(
885883
r: Q,
886884
m: M,
@@ -893,16 +891,40 @@ pub fn expect_passes_rule_with_schema<'a, Q, M, Sub, V, F, S>(
893891
M: GraphQLType<S, TypeInfo = ()>,
894892
Sub: GraphQLType<S, TypeInfo = ()>,
895893
V: Visitor<'a, S> + 'a,
896-
F: Fn() -> V,
894+
F: FnOnce() -> V,
897895
{
898-
let errs = validate(r, m, s, q, factory);
896+
let errs = validate(r, m, s, q, move |ctx, doc| {
897+
let mut mv = MultiVisitorNil.with(factory());
898+
visit(&mut mv, ctx, unsafe { ::std::mem::transmute(doc) });
899+
});
899900

900901
if !errs.is_empty() {
901902
print_errors(&errs);
902903
panic!("Expected rule to pass, but errors found");
903904
}
904905
}
905906

907+
pub fn expect_passes_fn_with_schema<'a, Q, M, Sub, F, S>(
908+
r: Q,
909+
m: M,
910+
s: Sub,
911+
visit_fn: F,
912+
q: &'a str,
913+
) where
914+
S: ScalarValue + 'a,
915+
Q: GraphQLType<S, TypeInfo = ()>,
916+
M: GraphQLType<S, TypeInfo = ()>,
917+
Sub: GraphQLType<S, TypeInfo = ()>,
918+
F: FnOnce(&mut ValidatorContext<'a, S>, &'a Document<S>),
919+
{
920+
let errs = validate(r, m, s, q, visit_fn);
921+
922+
if !errs.is_empty() {
923+
print_errors(&errs);
924+
panic!("Expected `visit_fn` to pass, but errors found");
925+
}
926+
}
927+
906928
pub fn expect_fails_rule<'a, V, F, S>(factory: F, q: &'a str, expected_errors: &[RuleError])
907929
where
908930
S: ScalarValue + 'a,
@@ -912,6 +934,14 @@ where
912934
expect_fails_rule_with_schema(QueryRoot, MutationRoot, factory, q, expected_errors);
913935
}
914936

937+
pub fn expect_fails_fn<'a, F, S>(visit_fn: F, q: &'a str, expected_errors: &[RuleError])
938+
where
939+
S: ScalarValue + 'a,
940+
F: FnOnce(&mut ValidatorContext<'a, S>, &'a Document<S>),
941+
{
942+
expect_fails_fn_with_schema(QueryRoot, MutationRoot, visit_fn, q, expected_errors);
943+
}
944+
915945
pub fn expect_fails_rule_with_schema<'a, Q, M, V, F, S>(
916946
r: Q,
917947
m: M,
@@ -923,9 +953,18 @@ pub fn expect_fails_rule_with_schema<'a, Q, M, V, F, S>(
923953
Q: GraphQLType<S, TypeInfo = ()>,
924954
M: GraphQLType<S, TypeInfo = ()>,
925955
V: Visitor<'a, S> + 'a,
926-
F: Fn() -> V,
927-
{
928-
let errs = validate(r, m, crate::EmptySubscription::<S>::new(), q, factory);
956+
F: FnOnce() -> V,
957+
{
958+
let errs = validate(
959+
r,
960+
m,
961+
crate::EmptySubscription::<S>::new(),
962+
q,
963+
move |ctx, doc| {
964+
let mut mv = MultiVisitorNil.with(factory());
965+
visit(&mut mv, ctx, unsafe { ::std::mem::transmute(doc) });
966+
},
967+
);
929968

930969
if errs.is_empty() {
931970
panic!("Expected rule to fail, but no errors were found");
@@ -940,6 +979,33 @@ pub fn expect_fails_rule_with_schema<'a, Q, M, V, F, S>(
940979
}
941980
}
942981

982+
pub fn expect_fails_fn_with_schema<'a, Q, M, F, S>(
983+
r: Q,
984+
m: M,
985+
visit_fn: F,
986+
q: &'a str,
987+
expected_errors: &[RuleError],
988+
) where
989+
S: ScalarValue + 'a,
990+
Q: GraphQLType<S, TypeInfo = ()>,
991+
M: GraphQLType<S, TypeInfo = ()>,
992+
F: FnOnce(&mut ValidatorContext<'a, S>, &'a Document<S>),
993+
{
994+
let errs = validate(r, m, crate::EmptySubscription::<S>::new(), q, visit_fn);
995+
996+
if errs.is_empty() {
997+
panic!("Expected `visit_fn`` to fail, but no errors were found");
998+
} else if errs != expected_errors {
999+
println!("==> Expected errors:");
1000+
print_errors(expected_errors);
1001+
1002+
println!("\n==> Actual errors:");
1003+
print_errors(&errs);
1004+
1005+
panic!("Unexpected set of errors found");
1006+
}
1007+
}
1008+
9431009
fn print_errors(errs: &[RuleError]) {
9441010
for err in errs {
9451011
for p in err.locations() {

0 commit comments

Comments
 (0)