Skip to content

Commit 23d352f

Browse files
authored
Support Max validation errors being produced (#2553)
* Limit the number of validation messages that can be produced * Limit the number of validation messages that can be produced and add and extra one ot say we did * Limit the number of validation messages that can be produced to 100
1 parent 84984aa commit 23d352f

5 files changed

Lines changed: 99 additions & 7 deletions

File tree

src/main/java/graphql/validation/ValidationErrorCollector.java

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,46 @@
66
import java.util.ArrayList;
77
import java.util.List;
88

9+
import static graphql.validation.ValidationErrorType.MaxValidationErrorsReached;
10+
911
@Internal
1012
public class ValidationErrorCollector {
1113

1214
private final List<ValidationError> errors = new ArrayList<>();
15+
private final int maxErrors;
16+
17+
public ValidationErrorCollector() {
18+
this(Validator.MAX_VALIDATION_ERRORS);
19+
}
20+
21+
public ValidationErrorCollector(int maxErrors) {
22+
this.maxErrors = maxErrors;
23+
}
24+
25+
private boolean atMaxErrors() {
26+
return errors.size() >= maxErrors - 1;
27+
}
1328

14-
public void addError(ValidationError validationError) {
15-
this.errors.add(validationError);
29+
/**
30+
* This will throw {@link MaxValidationErrorsReached} if too many validation errors are added
31+
*
32+
* @param validationError the error to add
33+
*
34+
* @throws MaxValidationErrorsReached if too many errors have been generated
35+
*/
36+
public void addError(ValidationError validationError) throws MaxValidationErrorsReached {
37+
if (!atMaxErrors()) {
38+
this.errors.add(validationError);
39+
} else {
40+
this.errors.add(ValidationError.newValidationError()
41+
.validationErrorType(MaxValidationErrorsReached)
42+
.description(
43+
String.format("The maximum number of validation errors has been reached. (%d)", maxErrors)
44+
)
45+
.build());
46+
47+
throw new MaxValidationErrorsReached();
48+
}
1649
}
1750

1851
public List<ValidationError> getErrors() {
@@ -38,4 +71,17 @@ public String toString() {
3871
"errors=" + errors +
3972
'}';
4073
}
74+
75+
/**
76+
* Indicates that that maximum number of validation errors has been reached
77+
*/
78+
@Internal
79+
static class MaxValidationErrorsReached extends RuntimeException {
80+
81+
@Override
82+
public synchronized Throwable fillInStackTrace() {
83+
return this;
84+
}
85+
}
86+
4187
}

src/main/java/graphql/validation/ValidationErrorType.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
public enum ValidationErrorType {
55

6+
MaxValidationErrorsReached,
67
DefaultForNonNullArgument,
78
WrongType,
89
UnknownType,

src/main/java/graphql/validation/Validator.java

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,36 @@
3636
@Internal
3737
public class Validator {
3838

39+
static int MAX_VALIDATION_ERRORS = 100;
40+
41+
/**
42+
* `graphql-java` will stop validation after a maximum number of validation messages has been reached. Attackers
43+
* can send pathologically invalid queries to induce a Denial of Service attack and fill memory with 10000s of errors
44+
* and burn CPU in process.
45+
*
46+
* By default, this is set to 100 errors. You can set a new JVM wide value as the maximum allowed validation errors.
47+
*
48+
* @param maxValidationErrors the maximum validation errors allow JVM wide
49+
*/
50+
public static void setMaxValidationErrors(int maxValidationErrors) {
51+
MAX_VALIDATION_ERRORS = maxValidationErrors;
52+
}
53+
54+
public static int getMaxValidationErrors() {
55+
return MAX_VALIDATION_ERRORS;
56+
}
57+
3958
public List<ValidationError> validateDocument(GraphQLSchema schema, Document document) {
4059
ValidationContext validationContext = new ValidationContext(schema, document);
4160

42-
43-
ValidationErrorCollector validationErrorCollector = new ValidationErrorCollector();
61+
ValidationErrorCollector validationErrorCollector = new ValidationErrorCollector(MAX_VALIDATION_ERRORS);
4462
List<AbstractRule> rules = createRules(validationContext, validationErrorCollector);
4563
LanguageTraversal languageTraversal = new LanguageTraversal();
46-
languageTraversal.traverse(document, new RulesVisitor(validationContext, rules));
64+
try {
65+
languageTraversal.traverse(document, new RulesVisitor(validationContext, rules));
66+
} catch (ValidationErrorCollector.MaxValidationErrorsReached ignored) {
67+
// if we have generated enough errors, then we can shortcut out
68+
}
4769

4870
return validationErrorCollector.getErrors();
4971
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package graphql.validation
2+
3+
class MaxValidationErrorsTest extends SpecValidationBase {
4+
5+
def "The maximum number of validation messages is respected"() {
6+
def directives = "@lol" * 10000
7+
def query = """
8+
query lotsOfErrors {
9+
f $directives
10+
}
11+
"""
12+
when:
13+
def validationErrors = validate(query)
14+
15+
then:
16+
validationErrors.size() == 100
17+
18+
when: "we can set a new maximum"
19+
Validator.setMaxValidationErrors(10)
20+
validationErrors = validate(query)
21+
22+
then:
23+
validationErrors.size() == 10
24+
}
25+
}

src/test/java/benchmark/TypeDefinitionParserVersusSerializeBenchMark.java

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

33
import graphql.schema.idl.SchemaParser;
44
import graphql.schema.idl.TypeDefinitionRegistry;
5-
import org.jetbrains.annotations.NotNull;
65
import org.openjdk.jmh.annotations.Benchmark;
76
import org.openjdk.jmh.annotations.BenchmarkMode;
87
import org.openjdk.jmh.annotations.Measurement;
@@ -62,7 +61,6 @@ static TypeDefinitionRegistry serialise() {
6261
});
6362
}
6463

65-
@NotNull
6664
private static ByteArrayOutputStream serialisedRegistryStream(TypeDefinitionRegistry registryOut) {
6765
return asRTE(() -> {
6866
ByteArrayOutputStream baOS = new ByteArrayOutputStream();

0 commit comments

Comments
 (0)