From 772084a1807ca6101f331ea7362f91533f2c2cb6 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Thu, 21 Jul 2022 18:47:23 +1000 Subject: [PATCH 01/11] This stops DOS attacks by making the lexer stop early. --- .../parser/GraphqlAntlrToLanguage.java | 2 +- src/main/java/graphql/parser/Parser.java | 30 ++++-- .../java/graphql/parser/ParserOptions.java | 12 +++ .../java/graphql/parser/SafeTokenSource.java | 82 +++++++++++++++++ .../graphql/parser/SafeTokenSourceTest.groovy | 92 +++++++++++++++++++ 5 files changed, 207 insertions(+), 11 deletions(-) create mode 100644 src/main/java/graphql/parser/SafeTokenSource.java create mode 100644 src/test/groovy/graphql/parser/SafeTokenSourceTest.groovy diff --git a/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java b/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java index 564d7983e6..dd26fd5934 100644 --- a/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java +++ b/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java @@ -96,7 +96,7 @@ public GraphqlAntlrToLanguage(CommonTokenStream tokens, MultiSourceReader multiS public GraphqlAntlrToLanguage(CommonTokenStream tokens, MultiSourceReader multiSourceReader, ParserOptions parserOptions) { this.tokens = tokens; this.multiSourceReader = multiSourceReader; - this.parserOptions = parserOptions == null ? ParserOptions.getDefaultParserOptions() : parserOptions; + this.parserOptions = ParserOptions.orDefaultOnes(parserOptions); } public ParserOptions getParserOptions() { diff --git a/src/main/java/graphql/parser/Parser.java b/src/main/java/graphql/parser/Parser.java index 7d83474e7c..66d77fc92e 100644 --- a/src/main/java/graphql/parser/Parser.java +++ b/src/main/java/graphql/parser/Parser.java @@ -26,6 +26,7 @@ import java.io.UncheckedIOException; import java.util.List; import java.util.function.BiFunction; +import java.util.function.Consumer; /** * This can parse graphql syntax, both Query syntax and Schema Definition Language (SDL) syntax, into an @@ -222,7 +223,13 @@ public void syntaxError(Recognizer recognizer, Object offendingSymbol, int } }); - CommonTokenStream tokens = new CommonTokenStream(lexer); + // default in the parser options if they are not set + parserOptions = ParserOptions.orDefaultOnes(parserOptions); + + int maxTokens = parserOptions.getMaxTokens(); + Consumer onTooManyTokens = token -> throwCancelParseIfTooManyTokens(token, maxTokens, multiSourceReader); + SafeTokenSource safeTokenSource = new SafeTokenSource(lexer, maxTokens, onTooManyTokens); + CommonTokenStream tokens = new CommonTokenStream(safeTokenSource); GraphqlParser parser = new GraphqlParser(tokens); parser.removeErrorListeners(); @@ -295,21 +302,24 @@ public int getCharPositionInLine() { count++; if (count > maxTokens) { - String msg = String.format("More than %d parse tokens have been presented. To prevent Denial Of Service attacks, parsing has been cancelled.", maxTokens); - SourceLocation sourceLocation = null; - String offendingToken = null; - if (token != null) { - offendingToken = node.getText(); - sourceLocation = AntlrHelper.createSourceLocation(multiSourceReader, token.getLine(), token.getCharPositionInLine()); - } - - throw new ParseCancelledException(msg, sourceLocation, offendingToken); + throwCancelParseIfTooManyTokens(token, maxTokens, multiSourceReader); } } }; parser.addParseListener(listener); } + private void throwCancelParseIfTooManyTokens(Token token, int maxTokens, MultiSourceReader multiSourceReader) throws ParseCancelledException { + String msg = String.format("More than %d parse tokens have been presented. To prevent Denial Of Service attacks, parsing has been cancelled.", maxTokens); + SourceLocation sourceLocation = null; + String offendingToken = null; + if (token != null) { + offendingToken = token.getText(); + sourceLocation = AntlrHelper.createSourceLocation(multiSourceReader, token.getLine(), token.getCharPositionInLine()); + } + throw new ParseCancelledException(msg, sourceLocation, offendingToken); + } + /** * Allows you to override the ANTLR to AST code. * diff --git a/src/main/java/graphql/parser/ParserOptions.java b/src/main/java/graphql/parser/ParserOptions.java index 234605bca8..94cec8be24 100644 --- a/src/main/java/graphql/parser/ParserOptions.java +++ b/src/main/java/graphql/parser/ParserOptions.java @@ -65,6 +65,18 @@ public static void setDefaultParserOptions(ParserOptions options) { defaultJvmParserOptions = assertNotNull(options); } + /** + * This will return the passed in parser options or the system-wide default ones if they + * are null + * + * @param parserOptions the options to check + * + * @return a non-null set of parser options + */ + public static ParserOptions orDefaultOnes(ParserOptions parserOptions) { + return parserOptions == null ? getDefaultParserOptions() : parserOptions; + } + private final boolean captureIgnoredChars; private final boolean captureSourceLocation; private final boolean captureLineComments; diff --git a/src/main/java/graphql/parser/SafeTokenSource.java b/src/main/java/graphql/parser/SafeTokenSource.java new file mode 100644 index 0000000000..2f76dba4af --- /dev/null +++ b/src/main/java/graphql/parser/SafeTokenSource.java @@ -0,0 +1,82 @@ +package graphql.parser; + +import graphql.Internal; +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.TokenFactory; +import org.antlr.v4.runtime.TokenSource; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +/** + * This token source can wrap a lexer and if it asks for more than a maximum number of tokens + * the user can take some action, typically throw an exception to stop lexing. + * + * It tracks the maximum number per token channel, so we have 3 at the moment, and they will all be tracked. + * + * This is used to protect us from evil input. The lexer will eagerly try to find all tokens + * at times and certain inputs (directives butted together for example) will cause the lexer + * to keep doing work even though before the tokens are presented back to the parser + * and hence before it has a chance to stop work once too much as been done. + */ +@Internal +public class SafeTokenSource implements TokenSource { + private final TokenSource lexer; + private final int maxTokens; + private final Consumer whenMaxTokensExceeded; + private final Map channelCounts; + + public SafeTokenSource(TokenSource lexer, int maxTokens, Consumer whenMaxTokensExceeded) { + this.lexer = lexer; + this.maxTokens = maxTokens; + this.whenMaxTokensExceeded = whenMaxTokensExceeded; + this.channelCounts = new HashMap<>(); + } + + @Override + public Token nextToken() { + Token token = lexer.nextToken(); + if (token != null) { + int channel = token.getChannel(); + Integer currentCount = channelCounts.getOrDefault(channel, 0); + currentCount = currentCount + 1; + if (currentCount > maxTokens) { + whenMaxTokensExceeded.accept(token); + } + channelCounts.put(channel, currentCount); + } + return token; + } + + @Override + public int getLine() { + return lexer.getLine(); + } + + @Override + public int getCharPositionInLine() { + return lexer.getCharPositionInLine(); + } + + @Override + public CharStream getInputStream() { + return lexer.getInputStream(); + } + + @Override + public String getSourceName() { + return lexer.getSourceName(); + } + + @Override + public void setTokenFactory(TokenFactory factory) { + lexer.setTokenFactory(factory); + } + + @Override + public TokenFactory getTokenFactory() { + return lexer.getTokenFactory(); + } +} diff --git a/src/test/groovy/graphql/parser/SafeTokenSourceTest.groovy b/src/test/groovy/graphql/parser/SafeTokenSourceTest.groovy new file mode 100644 index 0000000000..4d6ef37d1f --- /dev/null +++ b/src/test/groovy/graphql/parser/SafeTokenSourceTest.groovy @@ -0,0 +1,92 @@ +package graphql.parser + +import graphql.parser.antlr.GraphqlLexer +import org.antlr.v4.runtime.CharStreams +import org.antlr.v4.runtime.Token +import spock.lang.Specification + +import java.util.function.Consumer + +class SafeTokenSourceTest extends Specification { + + private void consumeAllTokens(SafeTokenSource tokenSource) { + def nextToken = tokenSource.nextToken() + while (nextToken != null && nextToken.getType() != Token.EOF) { + nextToken = tokenSource.nextToken() + } + } + + private GraphqlLexer lexer(doc) { + def charStream = CharStreams.fromString(doc) + def graphqlLexer = new GraphqlLexer(charStream) + graphqlLexer + } + + def "can call back to the consumer when max whitespace tokens are encountered"() { + + def offendingText = " " * 1000 + GraphqlLexer graphqlLexer = lexer(""" + query foo { _typename $offendingText @lol@lol@lol } + """) + when: + Token offendingToken = null + Consumer onToMany = { token -> + offendingToken = token + throw new IllegalStateException("stop!") + } + def tokenSource = new SafeTokenSource(graphqlLexer, 1000, onToMany) + + consumeAllTokens(tokenSource) + assert false, "This is not meant to actually consume all tokens" + + then: + thrown(IllegalStateException) + offendingToken != null + offendingToken.getChannel() == 3 // whitespace + offendingToken.getText() == " " + } + + def "can call back to the consumer when max graphql tokens are encountered"() { + + def offendingText = "@lol" * 1000 + GraphqlLexer graphqlLexer = lexer(""" + query foo { _typename $offendingText } + """) + when: + Token offendingToken = null + Consumer onToMany = { token -> + offendingToken = token + throw new IllegalStateException("stop!") + } + def tokenSource = new SafeTokenSource(graphqlLexer, 1000, onToMany) + + consumeAllTokens(tokenSource) + assert false, "This is not meant to actually consume all tokens" + + then: + thrown(IllegalStateException) + offendingToken != null + offendingToken.getChannel() == 0 // grammar + } + + def "can safely get to the end of text if its ok"() { + + GraphqlLexer graphqlLexer = lexer(""" + query foo { _typename @lol@lol@lol } + """) + when: + Token offendingToken = null + Consumer onToMany = { token -> + offendingToken = token + throw new IllegalStateException("stop!") + } + def tokenSource = new SafeTokenSource(graphqlLexer, 1000, onToMany) + + consumeAllTokens(tokenSource) + + then: + noExceptionThrown() + offendingToken == null + } + +} From 2caa273c856fe72a4833fb163d082473e8103ed6 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Thu, 21 Jul 2022 21:00:57 +1000 Subject: [PATCH 02/11] This stops DOS attacks by making the lexer stop early. Added BadSituationsRunner --- .../graphql/parser/BadParserSituations.java | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 src/test/groovy/graphql/parser/BadParserSituations.java diff --git a/src/test/groovy/graphql/parser/BadParserSituations.java b/src/test/groovy/graphql/parser/BadParserSituations.java new file mode 100644 index 0000000000..4a4085a6d0 --- /dev/null +++ b/src/test/groovy/graphql/parser/BadParserSituations.java @@ -0,0 +1,106 @@ +package graphql.parser; + +import com.google.common.base.Strings; +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.GraphQL; +import graphql.GraphQLError; +import graphql.schema.GraphQLSchema; +import graphql.schema.StaticDataFetcher; +import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.SchemaGenerator; +import graphql.schema.idl.SchemaParser; +import graphql.schema.idl.TypeDefinitionRegistry; + +import java.io.OutputStream; +import java.io.PrintStream; +import java.time.Duration; +import java.util.List; + +import static graphql.schema.idl.RuntimeWiring.newRuntimeWiring; + +/** + * This is not a test - it's a program we can run to show the system reacts to certain bad inputs + */ +public class BadParserSituations { + static Integer STEP = 5000; + static Integer CHECKS_AMOUNT = 15; + + public static void main(String[] args) { + GraphQL graphQL = setupSchema(); + + System.setErr(toDevNull()); + + for (int runNumber = 1; runNumber <=2; runNumber++) { + // on the second run - have unlimited tokens + if (runNumber > 1) { + ParserOptions unlimitedTokens = ParserOptions.getDefaultParserOptions().transform(builder -> builder.maxTokens(Integer.MAX_VALUE)); + ParserOptions.setDefaultParserOptions(unlimitedTokens); + } + runScenarios("Whitespace Bad Payloads",runNumber, Strings.repeat(" ", 10), graphQL); + runScenarios("Grammar Directives Bad Payloads",runNumber, "@lol", graphQL); + runScenarios("Grammar Field Bad Payloads",runNumber, "f(id:null)", graphQL); + + } + + } + + private static void runScenarios(String scenarioName, int runNumber, String badPayload, GraphQL graphQL) { + long maxRuntime = 0; + for (int i = 1; i < CHECKS_AMOUNT; i++) { + + int howManyBadPayloads = i * STEP; + String repeatedPayload = Strings.repeat(badPayload, howManyBadPayloads); + String query = "query {__typename " + repeatedPayload + " }"; + + ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(query).build(); + long startTime = System.nanoTime(); + + ExecutionResult executionResult = graphQL.execute(executionInput); + + Duration duration = Duration.ofNanos(System.nanoTime() - startTime); + + System.out.printf("%s(run #%d)(%d of %d) - | query length %d | bad payloads %d | duration %dms \n", scenarioName, runNumber, i, CHECKS_AMOUNT, query.length(), howManyBadPayloads, duration.toMillis()); + printLastError(executionResult.getErrors()); + + if (duration.toMillis() > maxRuntime) { + maxRuntime = duration.toMillis(); + } + } + System.out.printf("%s(run #%d) - finished | max time was %s ms \n" + + "=======================\n\n", scenarioName, runNumber, maxRuntime); + } + + private static void printLastError(List errors) { + if (errors.size() > 0) { + GraphQLError lastError = errors.get(errors.size() - 1); + System.out.printf("\terror : %s \n", lastError.getMessage()); + } + + } + + private static PrintStream toDevNull() { + return new PrintStream(new OutputStream() { + public void write(int b) { + //DO NOTHING + } + }); + } + + private static GraphQL setupSchema() { + String schema = "type Query{hello: String}"; + + SchemaParser schemaParser = new SchemaParser(); + TypeDefinitionRegistry typeDefinitionRegistry = schemaParser.parse(schema); + + RuntimeWiring runtimeWiring = newRuntimeWiring() + .type("Query", builder -> builder.dataFetcher("hello", new StaticDataFetcher("world"))) + .build(); + + SchemaGenerator schemaGenerator = new SchemaGenerator(); + GraphQLSchema graphQLSchema = schemaGenerator.makeExecutableSchema(typeDefinitionRegistry, runtimeWiring); + + GraphQL graphQL = GraphQL.newGraphQL(graphQLSchema).build(); + return graphQL; + } +} From 30ee65a3c3cb1526f0ecdf16b0a2a087516645da Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Thu, 21 Jul 2022 21:38:32 +1000 Subject: [PATCH 03/11] This stops DOS attacks by making the lexer stop early. Added BadSituationsRunner with comments --- .../graphql/parser/BadParserSituations.java | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/test/groovy/graphql/parser/BadParserSituations.java b/src/test/groovy/graphql/parser/BadParserSituations.java index 4a4085a6d0..e044f52fd8 100644 --- a/src/test/groovy/graphql/parser/BadParserSituations.java +++ b/src/test/groovy/graphql/parser/BadParserSituations.java @@ -16,6 +16,7 @@ import java.io.PrintStream; import java.time.Duration; import java.util.List; +import java.util.function.Function; import static graphql.schema.idl.RuntimeWiring.newRuntimeWiring; @@ -31,27 +32,40 @@ public static void main(String[] args) { System.setErr(toDevNull()); - for (int runNumber = 1; runNumber <=2; runNumber++) { + for (int runNumber = 1; runNumber <= 2; runNumber++) { // on the second run - have unlimited tokens if (runNumber > 1) { ParserOptions unlimitedTokens = ParserOptions.getDefaultParserOptions().transform(builder -> builder.maxTokens(Integer.MAX_VALUE)); ParserOptions.setDefaultParserOptions(unlimitedTokens); } - runScenarios("Whitespace Bad Payloads",runNumber, Strings.repeat(" ", 10), graphQL); - runScenarios("Grammar Directives Bad Payloads",runNumber, "@lol", graphQL); - runScenarios("Grammar Field Bad Payloads",runNumber, "f(id:null)", graphQL); + runScenarios("Comment Bad Payloads", runNumber, graphQL, howMany -> { + String repeatedPayload = Strings.repeat("# some comment\n", howMany); + String query = repeatedPayload + "\nquery q {__typename }"; + return query; + }); + runScenarios("Whitespace Bad Payloads", runNumber, graphQL, howMany -> { + String repeatedPayload = Strings.repeat(" ", howMany); + return "query {__typename " + repeatedPayload + " }"; + }); + runScenarios("Grammar Directives Bad Payloads", runNumber, graphQL, howMany -> { + String repeatedPayload = Strings.repeat("@lol", howMany); + return "query {__typename " + repeatedPayload + " }"; + }); + runScenarios("Grammar Field Bad Payloads", runNumber, graphQL, howMany -> { + String repeatedPayload = Strings.repeat("f(id:null)", howMany); + return "query {__typename " + repeatedPayload + " }"; + }); } } - private static void runScenarios(String scenarioName, int runNumber, String badPayload, GraphQL graphQL) { + private static void runScenarios(String scenarioName, int runNumber, GraphQL graphQL, Function queryGenerator) { long maxRuntime = 0; for (int i = 1; i < CHECKS_AMOUNT; i++) { int howManyBadPayloads = i * STEP; - String repeatedPayload = Strings.repeat(badPayload, howManyBadPayloads); - String query = "query {__typename " + repeatedPayload + " }"; + String query = queryGenerator.apply(howManyBadPayloads); ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(query).build(); long startTime = System.nanoTime(); From 79b989c582952426beb419888756ba75d56a5f98 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Fri, 22 Jul 2022 10:32:14 +1000 Subject: [PATCH 04/11] This stops DOS attacks by making the lexer stop early. Added per query jvm settings --- src/main/java/graphql/ParseAndValidate.java | 5 ++ .../parser/GraphqlAntlrToLanguage.java | 3 +- src/main/java/graphql/parser/Parser.java | 3 +- .../java/graphql/parser/ParserOptions.java | 41 +++++++++++--- .../graphql/parser/BadParserSituations.java | 30 +++++----- .../graphql/parser/ParserOptionsTest.groovy | 56 +++++++++++++++++++ 6 files changed, 114 insertions(+), 24 deletions(-) create mode 100644 src/test/groovy/graphql/parser/ParserOptionsTest.groovy diff --git a/src/main/java/graphql/ParseAndValidate.java b/src/main/java/graphql/ParseAndValidate.java index 8e42cdb2bf..b35d2d73d8 100644 --- a/src/main/java/graphql/ParseAndValidate.java +++ b/src/main/java/graphql/ParseAndValidate.java @@ -9,8 +9,11 @@ import graphql.validation.Validator; import java.util.List; +import java.util.Optional; import java.util.function.Predicate; +import static java.util.Optional.ofNullable; + /** * This class allows you to parse and validate a graphql query without executing it. It will tell you * if it's syntactically valid and also semantically valid according to the graphql specification @@ -58,6 +61,8 @@ public static ParseAndValidateResult parse(ExecutionInput executionInput) { // // we allow the caller to specify new parser options by context ParserOptions parserOptions = executionInput.getGraphQLContext().get(ParserOptions.class); + // we use the query parser options by default if they are not specified + parserOptions = ofNullable(parserOptions).orElse(ParserOptions.getDefaultQueryParserOptions()); Parser parser = new Parser(); Document document = parser.parseDocument(executionInput.getQuery(), parserOptions); return ParseAndValidateResult.newResult().document(document).variables(executionInput.getVariables()).build(); diff --git a/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java b/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java index dd26fd5934..266e562290 100644 --- a/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java +++ b/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java @@ -77,6 +77,7 @@ import static graphql.collect.ImmutableKit.map; import static graphql.parser.StringValueParsing.parseSingleQuotedString; import static graphql.parser.StringValueParsing.parseTripleQuotedString; +import static java.util.Optional.ofNullable; @Internal public class GraphqlAntlrToLanguage { @@ -96,7 +97,7 @@ public GraphqlAntlrToLanguage(CommonTokenStream tokens, MultiSourceReader multiS public GraphqlAntlrToLanguage(CommonTokenStream tokens, MultiSourceReader multiSourceReader, ParserOptions parserOptions) { this.tokens = tokens; this.multiSourceReader = multiSourceReader; - this.parserOptions = ParserOptions.orDefaultOnes(parserOptions); + this.parserOptions = ofNullable(parserOptions).orElse(ParserOptions.getDefaultParserOptions()); } public ParserOptions getParserOptions() { diff --git a/src/main/java/graphql/parser/Parser.java b/src/main/java/graphql/parser/Parser.java index 66d77fc92e..76868b8c20 100644 --- a/src/main/java/graphql/parser/Parser.java +++ b/src/main/java/graphql/parser/Parser.java @@ -25,6 +25,7 @@ import java.io.Reader; import java.io.UncheckedIOException; import java.util.List; +import java.util.Optional; import java.util.function.BiFunction; import java.util.function.Consumer; @@ -224,7 +225,7 @@ public void syntaxError(Recognizer recognizer, Object offendingSymbol, int }); // default in the parser options if they are not set - parserOptions = ParserOptions.orDefaultOnes(parserOptions); + parserOptions = Optional.ofNullable(parserOptions).orElse(ParserOptions.getDefaultParserOptions()); int maxTokens = parserOptions.getMaxTokens(); Consumer onTooManyTokens = token -> throwCancelParseIfTooManyTokens(token, maxTokens, multiSourceReader); diff --git a/src/main/java/graphql/parser/ParserOptions.java b/src/main/java/graphql/parser/ParserOptions.java index 94cec8be24..89921a6b9d 100644 --- a/src/main/java/graphql/parser/ParserOptions.java +++ b/src/main/java/graphql/parser/ParserOptions.java @@ -28,17 +28,23 @@ public class ParserOptions { .captureSourceLocation(true) .captureLineComments(true) .maxTokens(MAX_QUERY_TOKENS) // to prevent a billion laughs style attacks, we set a default for graphql-java + .build(); + private static ParserOptions defaultJvmQueryParserOptions = newParserOptions() + .captureIgnoredChars(false) + .captureSourceLocation(true) + .captureLineComments(false) // #comments are not useful in query parsing + .maxTokens(MAX_QUERY_TOKENS) // to prevent a billion laughs style attacks, we set a default for graphql-java .build(); /** - * By default the Parser will not capture ignored characters. A static holds this default + * By default, the Parser will not capture ignored characters. A static holds this default * value in a JVM wide basis options object. * * Significant memory savings can be made if we do NOT capture ignored characters, * especially in SDL parsing. * - * @return the static default value on whether to capture ignored chars + * @return the static default JVM value * * @see graphql.language.IgnoredChar * @see graphql.language.SourceLocation @@ -48,7 +54,20 @@ public static ParserOptions getDefaultParserOptions() { } /** - * By default the Parser will not capture ignored characters. A static holds this default + * By default, for query parsing, the Parser will not capture ignored characters and it will not capture line comments into AST + * elements . A static holds this default value for query parsing in a JVM wide basis options object. + * + * @return the static default JVM value for query parsing + * + * @see graphql.language.IgnoredChar + * @see graphql.language.SourceLocation + */ + public static ParserOptions getDefaultQueryParserOptions() { + return defaultJvmQueryParserOptions; + } + + /** + * By default, the Parser will not capture ignored characters. A static holds this default * value in a JVM wide basis options object. * * Significant memory savings can be made if we do NOT capture ignored characters, @@ -66,17 +85,21 @@ public static void setDefaultParserOptions(ParserOptions options) { } /** - * This will return the passed in parser options or the system-wide default ones if they - * are null + * By default, the Parser will not capture ignored characters or line comments. A static holds this default + * value in a JVM wide basis options object for query parsing. + * + * This static can be set to true to allow the behavior of version 16.x or before. * - * @param parserOptions the options to check + * @param options - the new default JVM parser options for query parsing * - * @return a non-null set of parser options + * @see graphql.language.IgnoredChar + * @see graphql.language.SourceLocation */ - public static ParserOptions orDefaultOnes(ParserOptions parserOptions) { - return parserOptions == null ? getDefaultParserOptions() : parserOptions; + public static void setDefaultQueryParserOptions(ParserOptions options) { + defaultJvmQueryParserOptions = assertNotNull(options); } + private final boolean captureIgnoredChars; private final boolean captureSourceLocation; private final boolean captureLineComments; diff --git a/src/test/groovy/graphql/parser/BadParserSituations.java b/src/test/groovy/graphql/parser/BadParserSituations.java index e044f52fd8..530ef41300 100644 --- a/src/test/groovy/graphql/parser/BadParserSituations.java +++ b/src/test/groovy/graphql/parser/BadParserSituations.java @@ -33,25 +33,29 @@ public static void main(String[] args) { System.setErr(toDevNull()); for (int runNumber = 1; runNumber <= 2; runNumber++) { + String runState = "Limited Tokens"; // on the second run - have unlimited tokens if (runNumber > 1) { - ParserOptions unlimitedTokens = ParserOptions.getDefaultParserOptions().transform(builder -> builder.maxTokens(Integer.MAX_VALUE)); - ParserOptions.setDefaultParserOptions(unlimitedTokens); + ParserOptions unlimitedTokens = ParserOptions.getDefaultQueryParserOptions().transform( + builder -> builder.maxTokens(Integer.MAX_VALUE)); + ParserOptions.setDefaultQueryParserOptions(unlimitedTokens); + + runState = "Unlimited Tokens"; } - runScenarios("Comment Bad Payloads", runNumber, graphQL, howMany -> { + runScenarios("Whitespace Bad Payloads", runState, graphQL, howMany -> { + String repeatedPayload = Strings.repeat(" ", howMany); + return "query {__typename " + repeatedPayload + " }"; + }); + runScenarios("Comment Bad Payloads", runState, graphQL, howMany -> { String repeatedPayload = Strings.repeat("# some comment\n", howMany); String query = repeatedPayload + "\nquery q {__typename }"; return query; }); - runScenarios("Whitespace Bad Payloads", runNumber, graphQL, howMany -> { - String repeatedPayload = Strings.repeat(" ", howMany); - return "query {__typename " + repeatedPayload + " }"; - }); - runScenarios("Grammar Directives Bad Payloads", runNumber, graphQL, howMany -> { + runScenarios("Grammar Directives Bad Payloads", runState, graphQL, howMany -> { String repeatedPayload = Strings.repeat("@lol", howMany); return "query {__typename " + repeatedPayload + " }"; }); - runScenarios("Grammar Field Bad Payloads", runNumber, graphQL, howMany -> { + runScenarios("Grammar Field Bad Payloads", runState, graphQL, howMany -> { String repeatedPayload = Strings.repeat("f(id:null)", howMany); return "query {__typename " + repeatedPayload + " }"; }); @@ -60,7 +64,7 @@ public static void main(String[] args) { } - private static void runScenarios(String scenarioName, int runNumber, GraphQL graphQL, Function queryGenerator) { + private static void runScenarios(String scenarioName, String runState, GraphQL graphQL, Function queryGenerator) { long maxRuntime = 0; for (int i = 1; i < CHECKS_AMOUNT; i++) { @@ -74,15 +78,15 @@ private static void runScenarios(String scenarioName, int runNumber, GraphQL gra Duration duration = Duration.ofNanos(System.nanoTime() - startTime); - System.out.printf("%s(run #%d)(%d of %d) - | query length %d | bad payloads %d | duration %dms \n", scenarioName, runNumber, i, CHECKS_AMOUNT, query.length(), howManyBadPayloads, duration.toMillis()); + System.out.printf("%s(%s)(%d of %d) - | query length %d | bad payloads %d | duration %dms \n", scenarioName, runState, i, CHECKS_AMOUNT, query.length(), howManyBadPayloads, duration.toMillis()); printLastError(executionResult.getErrors()); if (duration.toMillis() > maxRuntime) { maxRuntime = duration.toMillis(); } } - System.out.printf("%s(run #%d) - finished | max time was %s ms \n" + - "=======================\n\n", scenarioName, runNumber, maxRuntime); + System.out.printf("%s(%s) - finished | max time was %s ms \n" + + "=======================\n\n", scenarioName, runState, maxRuntime); } private static void printLastError(List errors) { diff --git a/src/test/groovy/graphql/parser/ParserOptionsTest.groovy b/src/test/groovy/graphql/parser/ParserOptionsTest.groovy new file mode 100644 index 0000000000..ea954c05c3 --- /dev/null +++ b/src/test/groovy/graphql/parser/ParserOptionsTest.groovy @@ -0,0 +1,56 @@ +package graphql.parser + +import spock.lang.Specification + +class ParserOptionsTest extends Specification { + static defaultOptions = ParserOptions.getDefaultParserOptions() + static defaultQueryOptions = ParserOptions.getDefaultQueryParserOptions() + + void setup() { + ParserOptions.setDefaultParserOptions(defaultOptions) + ParserOptions.setDefaultQueryParserOptions(defaultQueryOptions) + } + + void cleanup() { + ParserOptions.setDefaultParserOptions(defaultOptions) + ParserOptions.setDefaultQueryParserOptions(defaultQueryOptions) + } + + def "lock in default settings"() { + expect: + defaultOptions.getMaxTokens() == 15_000 + defaultOptions.isCaptureSourceLocation() + defaultOptions.isCaptureLineComments() + !defaultOptions.isCaptureIgnoredChars() + + defaultQueryOptions.getMaxTokens() == 15_000 + defaultQueryOptions.isCaptureSourceLocation() + !defaultQueryOptions.isCaptureLineComments() + !defaultQueryOptions.isCaptureIgnoredChars() + } + + def "can set in new option JVM wide"() { + def newDefaultOptions = defaultOptions.transform({ it.captureIgnoredChars(true) }) + def newDefaultQueryOptions = defaultQueryOptions.transform({ it.captureIgnoredChars(true).captureLineComments(true) }) + + when: + ParserOptions.setDefaultParserOptions(newDefaultOptions) + ParserOptions.setDefaultQueryParserOptions(newDefaultQueryOptions) + + def currentDefaultOptions = ParserOptions.getDefaultParserOptions() + def currentDefaultQueryOptions = ParserOptions.getDefaultQueryParserOptions() + + then: + + currentDefaultOptions.getMaxTokens() == 15_000 + currentDefaultOptions.isCaptureSourceLocation() + currentDefaultOptions.isCaptureLineComments() + currentDefaultOptions.isCaptureIgnoredChars() + + currentDefaultQueryOptions.getMaxTokens() == 15_000 + currentDefaultQueryOptions.isCaptureSourceLocation() + currentDefaultQueryOptions.isCaptureLineComments() + currentDefaultQueryOptions.isCaptureIgnoredChars() + + } +} From 888d12380b43c6ced425c2f667542a16015e754b Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Fri, 22 Jul 2022 14:53:45 +1000 Subject: [PATCH 05/11] This stops DOS attacks by making the lexer stop early. Added whitespace counts separate from token counts --- src/main/java/graphql/ParseAndValidate.java | 3 +- .../parser/GraphqlAntlrToLanguage.java | 4 +- src/main/java/graphql/parser/Parser.java | 20 +++++-- .../java/graphql/parser/ParserOptions.java | 53 ++++++++++++++----- .../java/graphql/parser/SafeTokenSource.java | 25 +++++++-- .../graphql/parser/BadParserSituations.java | 10 ++-- .../graphql/parser/ParserOptionsTest.groovy | 34 ++++++------ .../groovy/graphql/parser/ParserTest.groovy | 32 +++++++++++ .../graphql/parser/SafeTokenSourceTest.groovy | 28 +++++----- 9 files changed, 153 insertions(+), 56 deletions(-) diff --git a/src/main/java/graphql/ParseAndValidate.java b/src/main/java/graphql/ParseAndValidate.java index b35d2d73d8..821eebade4 100644 --- a/src/main/java/graphql/ParseAndValidate.java +++ b/src/main/java/graphql/ParseAndValidate.java @@ -9,7 +9,6 @@ import graphql.validation.Validator; import java.util.List; -import java.util.Optional; import java.util.function.Predicate; import static java.util.Optional.ofNullable; @@ -62,7 +61,7 @@ public static ParseAndValidateResult parse(ExecutionInput executionInput) { // we allow the caller to specify new parser options by context ParserOptions parserOptions = executionInput.getGraphQLContext().get(ParserOptions.class); // we use the query parser options by default if they are not specified - parserOptions = ofNullable(parserOptions).orElse(ParserOptions.getDefaultQueryParserOptions()); + parserOptions = ofNullable(parserOptions).orElse(ParserOptions.getDefaultOperationParserOptions()); Parser parser = new Parser(); Document document = parser.parseDocument(executionInput.getQuery(), parserOptions); return ParseAndValidateResult.newResult().document(document).variables(executionInput.getVariables()).build(); diff --git a/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java b/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java index 266e562290..2bd8096392 100644 --- a/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java +++ b/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java @@ -75,6 +75,8 @@ import static graphql.Assert.assertShouldNeverHappen; import static graphql.collect.ImmutableKit.emptyList; import static graphql.collect.ImmutableKit.map; +import static graphql.parser.Parser.CHANNEL_COMMENTS; +import static graphql.parser.Parser.CHANNEL_IGNORED_CHARS; import static graphql.parser.StringValueParsing.parseSingleQuotedString; import static graphql.parser.StringValueParsing.parseTripleQuotedString; import static java.util.Optional.ofNullable; @@ -83,8 +85,6 @@ public class GraphqlAntlrToLanguage { private static final List NO_COMMENTS = ImmutableKit.emptyList(); - private static final int CHANNEL_COMMENTS = 2; - private static final int CHANNEL_IGNORED_CHARS = 3; private final CommonTokenStream tokens; private final MultiSourceReader multiSourceReader; private final ParserOptions parserOptions; diff --git a/src/main/java/graphql/parser/Parser.java b/src/main/java/graphql/parser/Parser.java index 76868b8c20..6856124ca6 100644 --- a/src/main/java/graphql/parser/Parser.java +++ b/src/main/java/graphql/parser/Parser.java @@ -1,5 +1,6 @@ package graphql.parser; +import graphql.Internal; import graphql.PublicApi; import graphql.language.Document; import graphql.language.Node; @@ -26,8 +27,8 @@ import java.io.UncheckedIOException; import java.util.List; import java.util.Optional; +import java.util.function.BiConsumer; import java.util.function.BiFunction; -import java.util.function.Consumer; /** * This can parse graphql syntax, both Query syntax and Schema Definition Language (SDL) syntax, into an @@ -48,6 +49,11 @@ @PublicApi public class Parser { + @Internal + public static final int CHANNEL_COMMENTS = 2; + @Internal + public static final int CHANNEL_IGNORED_CHARS = 3; + /** * Parses a string input into a graphql AST {@link Document} * @@ -227,9 +233,12 @@ public void syntaxError(Recognizer recognizer, Object offendingSymbol, int // default in the parser options if they are not set parserOptions = Optional.ofNullable(parserOptions).orElse(ParserOptions.getDefaultParserOptions()); + // this lexer wrapper allows us to stop lexing when too many tokens are in place. This prevents DOS attacks. int maxTokens = parserOptions.getMaxTokens(); - Consumer onTooManyTokens = token -> throwCancelParseIfTooManyTokens(token, maxTokens, multiSourceReader); - SafeTokenSource safeTokenSource = new SafeTokenSource(lexer, maxTokens, onTooManyTokens); + int maxWhitespaceTokens = parserOptions.getMaxWhitespaceTokens(); + BiConsumer onTooManyTokens = (maxTokenCount, token) -> throwCancelParseIfTooManyTokens(token, maxTokenCount, multiSourceReader); + SafeTokenSource safeTokenSource = new SafeTokenSource(lexer, maxTokens, maxWhitespaceTokens, onTooManyTokens); + CommonTokenStream tokens = new CommonTokenStream(safeTokenSource); GraphqlParser parser = new GraphqlParser(tokens); @@ -311,13 +320,16 @@ public int getCharPositionInLine() { } private void throwCancelParseIfTooManyTokens(Token token, int maxTokens, MultiSourceReader multiSourceReader) throws ParseCancelledException { - String msg = String.format("More than %d parse tokens have been presented. To prevent Denial Of Service attacks, parsing has been cancelled.", maxTokens); + String tokenType = "grammar"; SourceLocation sourceLocation = null; String offendingToken = null; if (token != null) { + tokenType = token.getChannel() == CHANNEL_IGNORED_CHARS ? "whitespace" : tokenType; + offendingToken = token.getText(); sourceLocation = AntlrHelper.createSourceLocation(multiSourceReader, token.getLine(), token.getCharPositionInLine()); } + String msg = String.format("More than %d %s tokens have been presented. To prevent Denial Of Service attacks, parsing has been cancelled.", maxTokens, tokenType); throw new ParseCancelledException(msg, sourceLocation, offendingToken); } diff --git a/src/main/java/graphql/parser/ParserOptions.java b/src/main/java/graphql/parser/ParserOptions.java index 89921a6b9d..9236e8f191 100644 --- a/src/main/java/graphql/parser/ParserOptions.java +++ b/src/main/java/graphql/parser/ParserOptions.java @@ -13,15 +13,24 @@ public class ParserOptions { /** - * An graphql hacking vector is to send nonsensical queries that burn lots of parsing CPU time and burn - * memory representing a document that wont ever execute. To prevent this for most users, graphql-java + * A graphql hacking vector is to send nonsensical queries that burn lots of parsing CPU time and burn + * memory representing a document that won't ever execute. To prevent this for most users, graphql-java * set this value to 15000. ANTLR parsing time is linear to the number of tokens presented. The more you * allow the longer it takes. * * If you want to allow more, then {@link #setDefaultParserOptions(ParserOptions)} allows you to change this * JVM wide. */ - public static final int MAX_QUERY_TOKENS = 15000; + public static final int MAX_QUERY_TOKENS = 15_000; + /** + * Another graphql hacking vector is to send large amounts of whitespace in queries that burn lots of parsing CPU time and burn + * memory representing a document. Whitespace token processing in ANTLR is 2 order of magnitude faster than grammar token processing + * however it still takes some time to happen. + * + * If you want to allow more, then {@link #setDefaultParserOptions(ParserOptions)} allows you to change this + * JVM wide. + */ + public static final int MAX_WHITESPACE_TOKENS = 200_000; private static ParserOptions defaultJvmParserOptions = newParserOptions() .captureIgnoredChars(false) @@ -30,7 +39,7 @@ public class ParserOptions { .maxTokens(MAX_QUERY_TOKENS) // to prevent a billion laughs style attacks, we set a default for graphql-java .build(); - private static ParserOptions defaultJvmQueryParserOptions = newParserOptions() + private static ParserOptions defaultJvmOperationParserOptions = newParserOptions() .captureIgnoredChars(false) .captureSourceLocation(true) .captureLineComments(false) // #comments are not useful in query parsing @@ -54,16 +63,16 @@ public static ParserOptions getDefaultParserOptions() { } /** - * By default, for query parsing, the Parser will not capture ignored characters and it will not capture line comments into AST - * elements . A static holds this default value for query parsing in a JVM wide basis options object. + * By default, for operation parsing, the Parser will not capture ignored characters, and it will not capture line comments into AST + * elements . A static holds this default value for operation parsing in a JVM wide basis options object. * * @return the static default JVM value for query parsing * * @see graphql.language.IgnoredChar * @see graphql.language.SourceLocation */ - public static ParserOptions getDefaultQueryParserOptions() { - return defaultJvmQueryParserOptions; + public static ParserOptions getDefaultOperationParserOptions() { + return defaultJvmOperationParserOptions; } /** @@ -86,17 +95,17 @@ public static void setDefaultParserOptions(ParserOptions options) { /** * By default, the Parser will not capture ignored characters or line comments. A static holds this default - * value in a JVM wide basis options object for query parsing. + * value in a JVM wide basis options object for operation parsing. * * This static can be set to true to allow the behavior of version 16.x or before. * - * @param options - the new default JVM parser options for query parsing + * @param options - the new default JVM parser options for operation parsing * * @see graphql.language.IgnoredChar * @see graphql.language.SourceLocation */ - public static void setDefaultQueryParserOptions(ParserOptions options) { - defaultJvmQueryParserOptions = assertNotNull(options); + public static void setDefaultOperationParserOptions(ParserOptions options) { + defaultJvmOperationParserOptions = assertNotNull(options); } @@ -104,6 +113,7 @@ public static void setDefaultQueryParserOptions(ParserOptions options) { private final boolean captureSourceLocation; private final boolean captureLineComments; private final int maxTokens; + private final int maxWhitespaceTokens; private final ParsingListener parsingListener; private ParserOptions(Builder builder) { @@ -111,6 +121,7 @@ private ParserOptions(Builder builder) { this.captureSourceLocation = builder.captureSourceLocation; this.captureLineComments = builder.captureLineComments; this.maxTokens = builder.maxTokens; + this.maxWhitespaceTokens = builder.maxWhitespaceTokens; this.parsingListener = builder.parsingListener; } @@ -162,6 +173,17 @@ public int getMaxTokens() { return maxTokens; } + /** + * A graphql hacking vector is to send larges amounts of whitespace that burn lots of parsing CPU time and burn + * memory representing a document. To prevent this you can set a maximum number of whitepsace parse + * tokens that will be accepted before an exception is thrown and the parsing is stopped. + * + * @return the maximum number of raw whitespace tokens the parser will accept, after which an exception will be thrown. + */ + public int getMaxWhitespaceTokens() { + return maxWhitespaceTokens; + } + public ParsingListener getParsingListener() { return parsingListener; } @@ -183,6 +205,7 @@ public static class Builder { private boolean captureLineComments = true; private int maxTokens = MAX_QUERY_TOKENS; private ParsingListener parsingListener = ParsingListener.NOOP; + private int maxWhitespaceTokens = MAX_WHITESPACE_TOKENS; Builder() { } @@ -192,6 +215,7 @@ public static class Builder { this.captureSourceLocation = parserOptions.captureSourceLocation; this.captureLineComments = parserOptions.captureLineComments; this.maxTokens = parserOptions.maxTokens; + this.maxWhitespaceTokens = parserOptions.maxWhitespaceTokens; this.parsingListener = parserOptions.parsingListener; } @@ -215,6 +239,11 @@ public Builder maxTokens(int maxTokens) { return this; } + public Builder maxWhitespaceTokens(int maxWhitespaceTokens) { + this.maxWhitespaceTokens = maxWhitespaceTokens; + return this; + } + public Builder parsingListener(ParsingListener parsingListener) { this.parsingListener = assertNotNull(parsingListener); return this; diff --git a/src/main/java/graphql/parser/SafeTokenSource.java b/src/main/java/graphql/parser/SafeTokenSource.java index 2f76dba4af..bad34a3728 100644 --- a/src/main/java/graphql/parser/SafeTokenSource.java +++ b/src/main/java/graphql/parser/SafeTokenSource.java @@ -8,6 +8,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.function.BiConsumer; import java.util.function.Consumer; /** @@ -23,33 +24,47 @@ */ @Internal public class SafeTokenSource implements TokenSource { + private final TokenSource lexer; private final int maxTokens; - private final Consumer whenMaxTokensExceeded; + private final int maxWhitespaceTokens; + private final BiConsumer whenMaxTokensExceeded; private final Map channelCounts; - public SafeTokenSource(TokenSource lexer, int maxTokens, Consumer whenMaxTokensExceeded) { + public SafeTokenSource(TokenSource lexer, int maxTokens, int maxWhitespaceTokens, BiConsumer whenMaxTokensExceeded) { this.lexer = lexer; this.maxTokens = maxTokens; + this.maxWhitespaceTokens = maxWhitespaceTokens; this.whenMaxTokensExceeded = whenMaxTokensExceeded; this.channelCounts = new HashMap<>(); } + + @Override public Token nextToken() { Token token = lexer.nextToken(); if (token != null) { int channel = token.getChannel(); - Integer currentCount = channelCounts.getOrDefault(channel, 0); + int currentCount = channelCounts.getOrDefault(channel, 0); currentCount = currentCount + 1; - if (currentCount > maxTokens) { - whenMaxTokensExceeded.accept(token); + if (channel == Parser.CHANNEL_IGNORED_CHARS) { + // whitespace gets its own max count + callbackIfMaxExceeded(maxWhitespaceTokens, currentCount, token); + } else { + callbackIfMaxExceeded(maxTokens, currentCount, token); } channelCounts.put(channel, currentCount); } return token; } + private void callbackIfMaxExceeded(int maxCount, int currentCount, Token token) { + if (currentCount > maxCount) { + whenMaxTokensExceeded.accept(maxCount,token); + } + } + @Override public int getLine() { return lexer.getLine(); diff --git a/src/test/groovy/graphql/parser/BadParserSituations.java b/src/test/groovy/graphql/parser/BadParserSituations.java index 530ef41300..9f603ccee9 100644 --- a/src/test/groovy/graphql/parser/BadParserSituations.java +++ b/src/test/groovy/graphql/parser/BadParserSituations.java @@ -22,6 +22,10 @@ /** * This is not a test - it's a program we can run to show the system reacts to certain bad inputs + * + * You can run this to discover scenarios and see what happens at what levels. + * + * I used this to help discover more on the behavior of ANTLR and its moving parts */ public class BadParserSituations { static Integer STEP = 5000; @@ -36,9 +40,9 @@ public static void main(String[] args) { String runState = "Limited Tokens"; // on the second run - have unlimited tokens if (runNumber > 1) { - ParserOptions unlimitedTokens = ParserOptions.getDefaultQueryParserOptions().transform( - builder -> builder.maxTokens(Integer.MAX_VALUE)); - ParserOptions.setDefaultQueryParserOptions(unlimitedTokens); + ParserOptions unlimitedTokens = ParserOptions.getDefaultOperationParserOptions().transform( + builder -> builder.maxTokens(Integer.MAX_VALUE).maxWhitespaceTokens(Integer.MAX_VALUE)); + ParserOptions.setDefaultOperationParserOptions(unlimitedTokens); runState = "Unlimited Tokens"; } diff --git a/src/test/groovy/graphql/parser/ParserOptionsTest.groovy b/src/test/groovy/graphql/parser/ParserOptionsTest.groovy index ea954c05c3..f5c2aabd99 100644 --- a/src/test/groovy/graphql/parser/ParserOptionsTest.groovy +++ b/src/test/groovy/graphql/parser/ParserOptionsTest.groovy @@ -4,53 +4,57 @@ import spock.lang.Specification class ParserOptionsTest extends Specification { static defaultOptions = ParserOptions.getDefaultParserOptions() - static defaultQueryOptions = ParserOptions.getDefaultQueryParserOptions() + static defaultOperationOptions = ParserOptions.getDefaultOperationParserOptions() void setup() { ParserOptions.setDefaultParserOptions(defaultOptions) - ParserOptions.setDefaultQueryParserOptions(defaultQueryOptions) + ParserOptions.setDefaultOperationParserOptions(defaultOperationOptions) } void cleanup() { ParserOptions.setDefaultParserOptions(defaultOptions) - ParserOptions.setDefaultQueryParserOptions(defaultQueryOptions) + ParserOptions.setDefaultOperationParserOptions(defaultOperationOptions) } def "lock in default settings"() { expect: defaultOptions.getMaxTokens() == 15_000 + defaultOptions.getMaxWhitespaceTokens() == 200_000 defaultOptions.isCaptureSourceLocation() defaultOptions.isCaptureLineComments() !defaultOptions.isCaptureIgnoredChars() - defaultQueryOptions.getMaxTokens() == 15_000 - defaultQueryOptions.isCaptureSourceLocation() - !defaultQueryOptions.isCaptureLineComments() - !defaultQueryOptions.isCaptureIgnoredChars() + defaultOperationOptions.getMaxTokens() == 15_000 + defaultOperationOptions.getMaxWhitespaceTokens() == 200_000 + defaultOperationOptions.isCaptureSourceLocation() + !defaultOperationOptions.isCaptureLineComments() + !defaultOperationOptions.isCaptureIgnoredChars() } def "can set in new option JVM wide"() { def newDefaultOptions = defaultOptions.transform({ it.captureIgnoredChars(true) }) - def newDefaultQueryOptions = defaultQueryOptions.transform({ it.captureIgnoredChars(true).captureLineComments(true) }) + def newDefaultOperationOptions = defaultOperationOptions.transform( + { it.captureIgnoredChars(true).captureLineComments(true).maxWhitespaceTokens(300_000) }) when: ParserOptions.setDefaultParserOptions(newDefaultOptions) - ParserOptions.setDefaultQueryParserOptions(newDefaultQueryOptions) + ParserOptions.setDefaultOperationParserOptions(newDefaultOperationOptions) def currentDefaultOptions = ParserOptions.getDefaultParserOptions() - def currentDefaultQueryOptions = ParserOptions.getDefaultQueryParserOptions() + def currentDefaultOperationOptions = ParserOptions.getDefaultOperationParserOptions() then: currentDefaultOptions.getMaxTokens() == 15_000 + currentDefaultOptions.getMaxWhitespaceTokens() == 200_000 currentDefaultOptions.isCaptureSourceLocation() currentDefaultOptions.isCaptureLineComments() currentDefaultOptions.isCaptureIgnoredChars() - currentDefaultQueryOptions.getMaxTokens() == 15_000 - currentDefaultQueryOptions.isCaptureSourceLocation() - currentDefaultQueryOptions.isCaptureLineComments() - currentDefaultQueryOptions.isCaptureIgnoredChars() - + currentDefaultOperationOptions.getMaxTokens() == 15_000 + currentDefaultOperationOptions.getMaxWhitespaceTokens() == 300_000 + currentDefaultOperationOptions.isCaptureSourceLocation() + currentDefaultOperationOptions.isCaptureLineComments() + currentDefaultOperationOptions.isCaptureIgnoredChars() } } diff --git a/src/test/groovy/graphql/parser/ParserTest.groovy b/src/test/groovy/graphql/parser/ParserTest.groovy index 77fa57ddd7..ea21f8c2bd 100644 --- a/src/test/groovy/graphql/parser/ParserTest.groovy +++ b/src/test/groovy/graphql/parser/ParserTest.groovy @@ -1151,6 +1151,26 @@ triple3 : """edge cases \\""" "" " \\"" \\" edge cases""" er.errors[0].message.contains("parsing has been cancelled") } + def "a large whitespace laughs attack will be prevented by default"() { + def spaces = " " * 300_000 + def text = "query { f $spaces }" + when: + Parser.parse(text) + + then: + def e = thrown(ParseCancelledException) + e.getMessage().contains("parsing has been cancelled") + + when: "integration test to prove it cancels by default" + + def sdl = """type Query { f : ID} """ + def graphQL = TestUtil.graphQL(sdl).build() + def er = graphQL.execute(text) + then: + er.errors.size() == 1 + er.errors[0].message.contains("parsing has been cancelled") + } + def "they can shoot themselves if they want to with large documents"() { def lol = "@lol" * 10000 // two tokens = 20000+ tokens def text = "query { f $lol }" @@ -1163,6 +1183,18 @@ triple3 : """edge cases \\""" "" " \\"" \\" edge cases""" doc != null } + def "they can shoot themselves if they want to with large documents with lots of whitespace"() { + def spaces = " " * 300_000 + def text = "query { f $spaces }" + + def options = ParserOptions.newParserOptions().maxWhitespaceTokens(Integer.MAX_VALUE).build() + when: + def doc = new Parser().parseDocument(text, options) + + then: + doc != null + } + def "they can set their own listener into action"() { def queryText = "query { f(arg : 1) }" diff --git a/src/test/groovy/graphql/parser/SafeTokenSourceTest.groovy b/src/test/groovy/graphql/parser/SafeTokenSourceTest.groovy index 4d6ef37d1f..cf8b34658e 100644 --- a/src/test/groovy/graphql/parser/SafeTokenSourceTest.groovy +++ b/src/test/groovy/graphql/parser/SafeTokenSourceTest.groovy @@ -5,7 +5,7 @@ import org.antlr.v4.runtime.CharStreams import org.antlr.v4.runtime.Token import spock.lang.Specification -import java.util.function.Consumer +import java.util.function.BiConsumer class SafeTokenSourceTest extends Specification { @@ -30,23 +30,24 @@ class SafeTokenSourceTest extends Specification { """) when: Token offendingToken = null - Consumer onToMany = { token -> + BiConsumer onTooManyTokens = { max, token -> offendingToken = token - throw new IllegalStateException("stop!") + throw new IllegalStateException("stop at $max") } - def tokenSource = new SafeTokenSource(graphqlLexer, 1000, onToMany) + def tokenSource = new SafeTokenSource(graphqlLexer, 50, 1000, onTooManyTokens) consumeAllTokens(tokenSource) assert false, "This is not meant to actually consume all tokens" then: - thrown(IllegalStateException) + def e = thrown(IllegalStateException) + e.message == "stop at 1000" offendingToken != null offendingToken.getChannel() == 3 // whitespace offendingToken.getText() == " " } - def "can call back to the consumer when max graphql tokens are encountered"() { + def "can call back to the consumer when max grammar tokens are encountered"() { def offendingText = "@lol" * 1000 GraphqlLexer graphqlLexer = lexer(""" @@ -54,17 +55,18 @@ class SafeTokenSourceTest extends Specification { """) when: Token offendingToken = null - Consumer onToMany = { token -> + BiConsumer onTooManyTokens = { max, token -> offendingToken = token - throw new IllegalStateException("stop!") + throw new IllegalStateException("stop at $max") } - def tokenSource = new SafeTokenSource(graphqlLexer, 1000, onToMany) + def tokenSource = new SafeTokenSource(graphqlLexer, 1000, 200_000, onTooManyTokens) consumeAllTokens(tokenSource) assert false, "This is not meant to actually consume all tokens" then: - thrown(IllegalStateException) + def e = thrown(IllegalStateException) + e.message == "stop at 1000" offendingToken != null offendingToken.getChannel() == 0 // grammar } @@ -76,11 +78,11 @@ class SafeTokenSourceTest extends Specification { """) when: Token offendingToken = null - Consumer onToMany = { token -> + BiConsumer onTooManyTokens = { max, token -> offendingToken = token - throw new IllegalStateException("stop!") + throw new IllegalStateException("stop at $max") } - def tokenSource = new SafeTokenSource(graphqlLexer, 1000, onToMany) + def tokenSource = new SafeTokenSource(graphqlLexer, 1000, 200_000, onTooManyTokens) consumeAllTokens(tokenSource) From 1669ca745630b6971dbef717eca7da442eef5d97 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Fri, 22 Jul 2022 14:59:48 +1000 Subject: [PATCH 06/11] This stops DOS attacks by making the lexer stop early. Added whitespace counts separate from token counts - tweaks --- src/main/java/graphql/parser/Parser.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/graphql/parser/Parser.java b/src/main/java/graphql/parser/Parser.java index 6856124ca6..8c4eb9e3dd 100644 --- a/src/main/java/graphql/parser/Parser.java +++ b/src/main/java/graphql/parser/Parser.java @@ -324,7 +324,8 @@ private void throwCancelParseIfTooManyTokens(Token token, int maxTokens, MultiSo SourceLocation sourceLocation = null; String offendingToken = null; if (token != null) { - tokenType = token.getChannel() == CHANNEL_IGNORED_CHARS ? "whitespace" : tokenType; + int channel = token.getChannel(); + tokenType = channel == CHANNEL_IGNORED_CHARS ? "whitespace" : (channel == CHANNEL_COMMENTS ? "comments" : "grammar"); offendingToken = token.getText(); sourceLocation = AntlrHelper.createSourceLocation(multiSourceReader, token.getLine(), token.getCharPositionInLine()); From 077e64a906708f6ff23108024af226adcb9c377f Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Fri, 22 Jul 2022 15:07:18 +1000 Subject: [PATCH 07/11] This stops DOS attacks by making the lexer stop early. Added whitespace counts separate from token counts - tweaks --- src/main/java/graphql/parser/ParserOptions.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/graphql/parser/ParserOptions.java b/src/main/java/graphql/parser/ParserOptions.java index 9236e8f191..dae6e10e0e 100644 --- a/src/main/java/graphql/parser/ParserOptions.java +++ b/src/main/java/graphql/parser/ParserOptions.java @@ -23,8 +23,8 @@ public class ParserOptions { */ public static final int MAX_QUERY_TOKENS = 15_000; /** - * Another graphql hacking vector is to send large amounts of whitespace in queries that burn lots of parsing CPU time and burn - * memory representing a document. Whitespace token processing in ANTLR is 2 order of magnitude faster than grammar token processing + * Another graphql hacking vector is to send large amounts of whitespace in operations that burn lots of parsing CPU time and burn + * memory representing a document. Whitespace token processing in ANTLR is 2 orders of magnitude faster than grammar token processing * however it still takes some time to happen. * * If you want to allow more, then {@link #setDefaultParserOptions(ParserOptions)} allows you to change this @@ -163,7 +163,7 @@ public boolean isCaptureLineComments() { } /** - * A graphql hacking vector is to send nonsensical queries that burn lots of parsing CPU time and burn + * A graphql hacking vector is to send nonsensical queries that burn lots of parsing CPU time and burns * memory representing a document that won't ever execute. To prevent this you can set a maximum number of parse * tokens that will be accepted before an exception is thrown and the parsing is stopped. * @@ -175,7 +175,7 @@ public int getMaxTokens() { /** * A graphql hacking vector is to send larges amounts of whitespace that burn lots of parsing CPU time and burn - * memory representing a document. To prevent this you can set a maximum number of whitepsace parse + * memory representing a document. To prevent this you can set a maximum number of whitespace parse * tokens that will be accepted before an exception is thrown and the parsing is stopped. * * @return the maximum number of raw whitespace tokens the parser will accept, after which an exception will be thrown. From 17ec04b8a1faa0af82fe0679702e961b1c5318cd Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Fri, 22 Jul 2022 15:09:33 +1000 Subject: [PATCH 08/11] This stops DOS attacks by making the lexer stop early. Added whitespace counts separate from token counts - tweaks --- src/main/java/graphql/parser/ParserOptions.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/graphql/parser/ParserOptions.java b/src/main/java/graphql/parser/ParserOptions.java index dae6e10e0e..bc37241fb3 100644 --- a/src/main/java/graphql/parser/ParserOptions.java +++ b/src/main/java/graphql/parser/ParserOptions.java @@ -37,6 +37,7 @@ public class ParserOptions { .captureSourceLocation(true) .captureLineComments(true) .maxTokens(MAX_QUERY_TOKENS) // to prevent a billion laughs style attacks, we set a default for graphql-java + .maxWhitespaceTokens(MAX_WHITESPACE_TOKENS) .build(); private static ParserOptions defaultJvmOperationParserOptions = newParserOptions() @@ -44,6 +45,7 @@ public class ParserOptions { .captureSourceLocation(true) .captureLineComments(false) // #comments are not useful in query parsing .maxTokens(MAX_QUERY_TOKENS) // to prevent a billion laughs style attacks, we set a default for graphql-java + .maxWhitespaceTokens(MAX_WHITESPACE_TOKENS) .build(); /** From a0a7cfd4be7a5848dcf17e46172f64ca2a84ac32 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sat, 23 Jul 2022 07:51:11 +1000 Subject: [PATCH 09/11] This stops DOS attacks by making the lexer stop early.Use array instead of map --- src/main/java/graphql/parser/SafeTokenSource.java | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/main/java/graphql/parser/SafeTokenSource.java b/src/main/java/graphql/parser/SafeTokenSource.java index bad34a3728..2ae16a685d 100644 --- a/src/main/java/graphql/parser/SafeTokenSource.java +++ b/src/main/java/graphql/parser/SafeTokenSource.java @@ -6,10 +6,7 @@ import org.antlr.v4.runtime.TokenFactory; import org.antlr.v4.runtime.TokenSource; -import java.util.HashMap; -import java.util.Map; import java.util.function.BiConsumer; -import java.util.function.Consumer; /** * This token source can wrap a lexer and if it asks for more than a maximum number of tokens @@ -29,39 +26,37 @@ public class SafeTokenSource implements TokenSource { private final int maxTokens; private final int maxWhitespaceTokens; private final BiConsumer whenMaxTokensExceeded; - private final Map channelCounts; + private final int channelCounts[]; public SafeTokenSource(TokenSource lexer, int maxTokens, int maxWhitespaceTokens, BiConsumer whenMaxTokensExceeded) { this.lexer = lexer; this.maxTokens = maxTokens; this.maxWhitespaceTokens = maxWhitespaceTokens; this.whenMaxTokensExceeded = whenMaxTokensExceeded; - this.channelCounts = new HashMap<>(); + // we only have 3 channels - but they are 0,2 and 3 so use 5 for safety - still faster than a map get/put + this.channelCounts = new int[]{0, 0, 0, 0, 0}; } - @Override public Token nextToken() { Token token = lexer.nextToken(); if (token != null) { int channel = token.getChannel(); - int currentCount = channelCounts.getOrDefault(channel, 0); - currentCount = currentCount + 1; + int currentCount = ++channelCounts[channel]; if (channel == Parser.CHANNEL_IGNORED_CHARS) { // whitespace gets its own max count callbackIfMaxExceeded(maxWhitespaceTokens, currentCount, token); } else { callbackIfMaxExceeded(maxTokens, currentCount, token); } - channelCounts.put(channel, currentCount); } return token; } private void callbackIfMaxExceeded(int maxCount, int currentCount, Token token) { if (currentCount > maxCount) { - whenMaxTokensExceeded.accept(maxCount,token); + whenMaxTokensExceeded.accept(maxCount, token); } } From 50206dd13d4deccd7e969dd4c2fdff23a115a871 Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Sat, 23 Jul 2022 14:22:40 +1000 Subject: [PATCH 10/11] This stops DOS attacks by making the lexer stop early.Use array instead of map with comments --- src/main/java/graphql/parser/SafeTokenSource.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/graphql/parser/SafeTokenSource.java b/src/main/java/graphql/parser/SafeTokenSource.java index 2ae16a685d..5ebbf0a3a9 100644 --- a/src/main/java/graphql/parser/SafeTokenSource.java +++ b/src/main/java/graphql/parser/SafeTokenSource.java @@ -33,7 +33,9 @@ public SafeTokenSource(TokenSource lexer, int maxTokens, int maxWhitespaceTokens this.maxTokens = maxTokens; this.maxWhitespaceTokens = maxWhitespaceTokens; this.whenMaxTokensExceeded = whenMaxTokensExceeded; + // this could be a Map however we want it to be faster as possible. // we only have 3 channels - but they are 0,2 and 3 so use 5 for safety - still faster than a map get/put + // if we ever add another channel beyond 5 it will IOBEx during tests so future changes will be handled before release! this.channelCounts = new int[]{0, 0, 0, 0, 0}; } From 0bd81e7c09fa60ad5a43ee119e4e271e69bc085e Mon Sep 17 00:00:00 2001 From: Brad Baker Date: Tue, 26 Jul 2022 15:28:43 +1000 Subject: [PATCH 11/11] PR feedback - renamed options and added SDL options --- .../parser/GraphqlAntlrToLanguage.java | 6 +- src/main/java/graphql/parser/Parser.java | 4 +- .../java/graphql/parser/ParserOptions.java | 66 +++++++++++++++---- .../java/graphql/parser/SafeTokenSource.java | 2 +- .../java/graphql/schema/idl/SchemaParser.java | 4 +- .../graphql/parser/ParserOptionsTest.groovy | 19 ++++++ .../schema/idl/SchemaParserTest.groovy | 1 + 7 files changed, 80 insertions(+), 22 deletions(-) diff --git a/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java b/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java index 2bd8096392..da0209c1f6 100644 --- a/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java +++ b/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java @@ -76,7 +76,7 @@ import static graphql.collect.ImmutableKit.emptyList; import static graphql.collect.ImmutableKit.map; import static graphql.parser.Parser.CHANNEL_COMMENTS; -import static graphql.parser.Parser.CHANNEL_IGNORED_CHARS; +import static graphql.parser.Parser.CHANNEL_WHITESPACE; import static graphql.parser.StringValueParsing.parseSingleQuotedString; import static graphql.parser.StringValueParsing.parseTripleQuotedString; import static java.util.Optional.ofNullable; @@ -791,12 +791,12 @@ private void addIgnoredChars(ParserRuleContext ctx, NodeBuilder nodeBuilder) { } Token start = ctx.getStart(); int tokenStartIndex = start.getTokenIndex(); - List leftChannel = tokens.getHiddenTokensToLeft(tokenStartIndex, CHANNEL_IGNORED_CHARS); + List leftChannel = tokens.getHiddenTokensToLeft(tokenStartIndex, CHANNEL_WHITESPACE); List ignoredCharsLeft = mapTokenToIgnoredChar(leftChannel); Token stop = ctx.getStop(); int tokenStopIndex = stop.getTokenIndex(); - List rightChannel = tokens.getHiddenTokensToRight(tokenStopIndex, CHANNEL_IGNORED_CHARS); + List rightChannel = tokens.getHiddenTokensToRight(tokenStopIndex, CHANNEL_WHITESPACE); List ignoredCharsRight = mapTokenToIgnoredChar(rightChannel); nodeBuilder.ignoredChars(new IgnoredChars(ignoredCharsLeft, ignoredCharsRight)); diff --git a/src/main/java/graphql/parser/Parser.java b/src/main/java/graphql/parser/Parser.java index 8c4eb9e3dd..d2726dda68 100644 --- a/src/main/java/graphql/parser/Parser.java +++ b/src/main/java/graphql/parser/Parser.java @@ -52,7 +52,7 @@ public class Parser { @Internal public static final int CHANNEL_COMMENTS = 2; @Internal - public static final int CHANNEL_IGNORED_CHARS = 3; + public static final int CHANNEL_WHITESPACE = 3; /** * Parses a string input into a graphql AST {@link Document} @@ -325,7 +325,7 @@ private void throwCancelParseIfTooManyTokens(Token token, int maxTokens, MultiSo String offendingToken = null; if (token != null) { int channel = token.getChannel(); - tokenType = channel == CHANNEL_IGNORED_CHARS ? "whitespace" : (channel == CHANNEL_COMMENTS ? "comments" : "grammar"); + tokenType = channel == CHANNEL_WHITESPACE ? "whitespace" : (channel == CHANNEL_COMMENTS ? "comments" : "grammar"); offendingToken = token.getText(); sourceLocation = AntlrHelper.createSourceLocation(multiSourceReader, token.getLine(), token.getCharPositionInLine()); diff --git a/src/main/java/graphql/parser/ParserOptions.java b/src/main/java/graphql/parser/ParserOptions.java index bc37241fb3..6fe708323a 100644 --- a/src/main/java/graphql/parser/ParserOptions.java +++ b/src/main/java/graphql/parser/ParserOptions.java @@ -48,6 +48,14 @@ public class ParserOptions { .maxWhitespaceTokens(MAX_WHITESPACE_TOKENS) .build(); + private static ParserOptions defaultJvmSdlParserOptions = newParserOptions() + .captureIgnoredChars(false) + .captureSourceLocation(true) + .captureLineComments(true) // #comments are useful in SDL parsing + .maxTokens(Integer.MAX_VALUE) // we are less worried about a billion laughs with SDL parsing since the call path is not facing attackers + .maxWhitespaceTokens(Integer.MAX_VALUE) + .build(); + /** * By default, the Parser will not capture ignored characters. A static holds this default * value in a JVM wide basis options object. @@ -64,19 +72,6 @@ public static ParserOptions getDefaultParserOptions() { return defaultJvmParserOptions; } - /** - * By default, for operation parsing, the Parser will not capture ignored characters, and it will not capture line comments into AST - * elements . A static holds this default value for operation parsing in a JVM wide basis options object. - * - * @return the static default JVM value for query parsing - * - * @see graphql.language.IgnoredChar - * @see graphql.language.SourceLocation - */ - public static ParserOptions getDefaultOperationParserOptions() { - return defaultJvmOperationParserOptions; - } - /** * By default, the Parser will not capture ignored characters. A static holds this default * value in a JVM wide basis options object. @@ -95,6 +90,20 @@ public static void setDefaultParserOptions(ParserOptions options) { defaultJvmParserOptions = assertNotNull(options); } + + /** + * By default, for operation parsing, the Parser will not capture ignored characters, and it will not capture line comments into AST + * elements . A static holds this default value for operation parsing in a JVM wide basis options object. + * + * @return the static default JVM value for operation parsing + * + * @see graphql.language.IgnoredChar + * @see graphql.language.SourceLocation + */ + public static ParserOptions getDefaultOperationParserOptions() { + return defaultJvmOperationParserOptions; + } + /** * By default, the Parser will not capture ignored characters or line comments. A static holds this default * value in a JVM wide basis options object for operation parsing. @@ -110,6 +119,37 @@ public static void setDefaultOperationParserOptions(ParserOptions options) { defaultJvmOperationParserOptions = assertNotNull(options); } + /** + * By default, for SDL parsing, the Parser will not capture ignored characters, but it will capture line comments into AST + * elements. The SDL default options allow unlimited tokens and whitespace, since a DOS attack vector is + * not commonly available via schema SDL parsing. + * + * A static holds this default value for SDL parsing in a JVM wide basis options object. + * + * @return the static default JVM value for SDL parsing + * + * @see graphql.language.IgnoredChar + * @see graphql.language.SourceLocation + * @see graphql.schema.idl.SchemaParser + */ + public static ParserOptions getDefaultSdlParserOptions() { + return defaultJvmSdlParserOptions; + } + + /** + * By default, for SDL parsing, the Parser will not capture ignored characters, but it will capture line comments into AST + * elements . A static holds this default value for operation parsing in a JVM wide basis options object. + * + * This static can be set to true to allow the behavior of version 16.x or before. + * + * @param options - the new default JVM parser options for operation parsing + * + * @see graphql.language.IgnoredChar + * @see graphql.language.SourceLocation + */ + public static void setDefaultSdlParserOptions(ParserOptions options) { + defaultJvmSdlParserOptions = assertNotNull(options); + } private final boolean captureIgnoredChars; private final boolean captureSourceLocation; diff --git a/src/main/java/graphql/parser/SafeTokenSource.java b/src/main/java/graphql/parser/SafeTokenSource.java index 5ebbf0a3a9..c92c76d916 100644 --- a/src/main/java/graphql/parser/SafeTokenSource.java +++ b/src/main/java/graphql/parser/SafeTokenSource.java @@ -46,7 +46,7 @@ public Token nextToken() { if (token != null) { int channel = token.getChannel(); int currentCount = ++channelCounts[channel]; - if (channel == Parser.CHANNEL_IGNORED_CHARS) { + if (channel == Parser.CHANNEL_WHITESPACE) { // whitespace gets its own max count callbackIfMaxExceeded(maxWhitespaceTokens, currentCount, token); } else { diff --git a/src/main/java/graphql/schema/idl/SchemaParser.java b/src/main/java/graphql/schema/idl/SchemaParser.java index 1d051119fd..097e5649f3 100644 --- a/src/main/java/graphql/schema/idl/SchemaParser.java +++ b/src/main/java/graphql/schema/idl/SchemaParser.java @@ -114,9 +114,7 @@ public TypeDefinitionRegistry parseImpl(Reader schemaInput) { private TypeDefinitionRegistry parseImpl(Reader schemaInput, ParserOptions parseOptions) { try { if (parseOptions == null) { - // for SDL we don't stop how many parser tokens there are - it's not the attack vector - // to be prevented compared to queries - parseOptions = ParserOptions.getDefaultParserOptions().transform(opts -> opts.maxTokens(Integer.MAX_VALUE)); + parseOptions = ParserOptions.getDefaultSdlParserOptions(); } Parser parser = new Parser(); Document document = parser.parseDocument(schemaInput, parseOptions); diff --git a/src/test/groovy/graphql/parser/ParserOptionsTest.groovy b/src/test/groovy/graphql/parser/ParserOptionsTest.groovy index f5c2aabd99..5867b181fc 100644 --- a/src/test/groovy/graphql/parser/ParserOptionsTest.groovy +++ b/src/test/groovy/graphql/parser/ParserOptionsTest.groovy @@ -5,15 +5,18 @@ import spock.lang.Specification class ParserOptionsTest extends Specification { static defaultOptions = ParserOptions.getDefaultParserOptions() static defaultOperationOptions = ParserOptions.getDefaultOperationParserOptions() + static defaultSdlOptions = ParserOptions.getDefaultSdlParserOptions() void setup() { ParserOptions.setDefaultParserOptions(defaultOptions) ParserOptions.setDefaultOperationParserOptions(defaultOperationOptions) + ParserOptions.setDefaultSdlParserOptions(defaultSdlOptions) } void cleanup() { ParserOptions.setDefaultParserOptions(defaultOptions) ParserOptions.setDefaultOperationParserOptions(defaultOperationOptions) + ParserOptions.setDefaultSdlParserOptions(defaultSdlOptions) } def "lock in default settings"() { @@ -29,19 +32,29 @@ class ParserOptionsTest extends Specification { defaultOperationOptions.isCaptureSourceLocation() !defaultOperationOptions.isCaptureLineComments() !defaultOperationOptions.isCaptureIgnoredChars() + + defaultSdlOptions.getMaxTokens() == Integer.MAX_VALUE + defaultSdlOptions.getMaxWhitespaceTokens() == Integer.MAX_VALUE + defaultSdlOptions.isCaptureSourceLocation() + defaultSdlOptions.isCaptureLineComments() + !defaultSdlOptions.isCaptureIgnoredChars() } def "can set in new option JVM wide"() { def newDefaultOptions = defaultOptions.transform({ it.captureIgnoredChars(true) }) def newDefaultOperationOptions = defaultOperationOptions.transform( { it.captureIgnoredChars(true).captureLineComments(true).maxWhitespaceTokens(300_000) }) + def newDefaultSDlOptions = defaultSdlOptions.transform( + { it.captureIgnoredChars(true).captureLineComments(true).maxWhitespaceTokens(300_000) }) when: ParserOptions.setDefaultParserOptions(newDefaultOptions) ParserOptions.setDefaultOperationParserOptions(newDefaultOperationOptions) + ParserOptions.setDefaultSdlParserOptions(newDefaultSDlOptions) def currentDefaultOptions = ParserOptions.getDefaultParserOptions() def currentDefaultOperationOptions = ParserOptions.getDefaultOperationParserOptions() + def currentDefaultSdlOptions = ParserOptions.getDefaultSdlParserOptions() then: @@ -56,5 +69,11 @@ class ParserOptionsTest extends Specification { currentDefaultOperationOptions.isCaptureSourceLocation() currentDefaultOperationOptions.isCaptureLineComments() currentDefaultOperationOptions.isCaptureIgnoredChars() + + currentDefaultSdlOptions.getMaxTokens() == Integer.MAX_VALUE + currentDefaultSdlOptions.getMaxWhitespaceTokens() == 300_000 + currentDefaultSdlOptions.isCaptureSourceLocation() + currentDefaultSdlOptions.isCaptureLineComments() + currentDefaultSdlOptions.isCaptureIgnoredChars() } } diff --git a/src/test/groovy/graphql/schema/idl/SchemaParserTest.groovy b/src/test/groovy/graphql/schema/idl/SchemaParserTest.groovy index 0dd515e1ae..8fcbcd44e1 100644 --- a/src/test/groovy/graphql/schema/idl/SchemaParserTest.groovy +++ b/src/test/groovy/graphql/schema/idl/SchemaParserTest.groovy @@ -343,6 +343,7 @@ class SchemaParserTest extends Specification { def sdl = "type Query {\n" for (int i = 0; i < 30000; i++) { sdl += " f" + i + " : ID\n" + sdl += " " * 10 // 10 whitespace as well } sdl += "}"