Skip to content

Commit cb88645

Browse files
authored
17.x port - Stop DOS attacks by making the lexer stop early on evil input (#2902)
* 17.x port - Stop DOS attacks by making the lexer stop early on evil input * This stops DOS attacks by making the lexer stop early. * This stops DOS attacks by making the lexer stop early. Added BadSituationsRunner * This stops DOS attacks by making the lexer stop early. Added BadSituationsRunner with comments * This stops DOS attacks by making the lexer stop early. Added per query jvm settings * This stops DOS attacks by making the lexer stop early. Added whitespace counts separate from token counts * This stops DOS attacks by making the lexer stop early. Added whitespace counts separate from token counts - tweaks * This stops DOS attacks by making the lexer stop early. Added whitespace counts separate from token counts - tweaks * This stops DOS attacks by making the lexer stop early. Added whitespace counts separate from token counts - tweaks * This stops DOS attacks by making the lexer stop early.Use array instead of map * This stops DOS attacks by making the lexer stop early.Use array instead of map with comments * PR feedback - renamed options and added SDL options # Conflicts: # src/main/java/graphql/ParseAndValidate.java # src/main/java/graphql/parser/GraphqlAntlrToLanguage.java # src/main/java/graphql/parser/Parser.java # src/main/java/graphql/parser/ParserOptions.java # src/main/java/graphql/schema/idl/SchemaParser.java # src/test/groovy/graphql/parser/ParserTest.groovy * PR build * Reset after set options
1 parent bf4e324 commit cb88645

12 files changed

Lines changed: 640 additions & 31 deletions

File tree

.github/workflows/pull_request.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ on:
77
pull_request:
88
branches:
99
- master
10+
- 17.x
1011
jobs:
1112
buildAndTest:
1213
runs-on: ubuntu-latest

src/main/java/graphql/ParseAndValidate.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
import graphql.language.Document;
44
import graphql.parser.InvalidSyntaxException;
55
import graphql.parser.Parser;
6+
import graphql.parser.ParserOptions;
67
import graphql.schema.GraphQLSchema;
78
import graphql.validation.ValidationError;
89
import graphql.validation.Validator;
910

1011
import java.util.List;
1112

13+
import static java.util.Optional.ofNullable;
14+
1215
/**
1316
* This class allows you to parse and validate a graphql query without executing it. It will tell you
1417
* if its syntactically valid and also semantically valid according to the graphql specification
@@ -42,8 +45,13 @@ public static ParseAndValidateResult parseAndValidate(GraphQLSchema graphQLSchem
4245
*/
4346
public static ParseAndValidateResult parse(ExecutionInput executionInput) {
4447
try {
48+
//
49+
// we allow the caller to specify new parser options by context
50+
ParserOptions parserOptions = executionInput.getGraphQLContext().get(ParserOptions.class);
51+
// we use the query parser options by default if they are not specified
52+
parserOptions = ofNullable(parserOptions).orElse(ParserOptions.getDefaultOperationParserOptions());
4553
Parser parser = new Parser();
46-
Document document = parser.parseDocument(executionInput.getQuery());
54+
Document document = parser.parseDocument(executionInput.getQuery(),parserOptions);
4755
return ParseAndValidateResult.newResult().document(document).variables(executionInput.getVariables()).build();
4856
} catch (InvalidSyntaxException e) {
4957
return ParseAndValidateResult.newResult().syntaxException(e).variables(executionInput.getVariables()).build();

src/main/java/graphql/parser/GraphqlAntlrToLanguage.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,16 @@
7676
import static graphql.Assert.assertShouldNeverHappen;
7777
import static graphql.collect.ImmutableKit.emptyList;
7878
import static graphql.collect.ImmutableKit.map;
79+
import static graphql.parser.Parser.CHANNEL_COMMENTS;
80+
import static graphql.parser.Parser.CHANNEL_WHITESPACE;
7981
import static graphql.parser.StringValueParsing.parseSingleQuotedString;
8082
import static graphql.parser.StringValueParsing.parseTripleQuotedString;
83+
import static java.util.Optional.ofNullable;
8184

8285
@Internal
8386
public class GraphqlAntlrToLanguage {
8487

85-
private static final int CHANNEL_COMMENTS = 2;
86-
private static final int CHANNEL_IGNORED_CHARS = 3;
88+
private static final List<Comment> NO_COMMENTS = ImmutableKit.emptyList();
8789
private final CommonTokenStream tokens;
8890
private final MultiSourceReader multiSourceReader;
8991
private final ParserOptions parserOptions;
@@ -96,7 +98,7 @@ public GraphqlAntlrToLanguage(CommonTokenStream tokens, MultiSourceReader multiS
9698
public GraphqlAntlrToLanguage(CommonTokenStream tokens, MultiSourceReader multiSourceReader, ParserOptions parserOptions) {
9799
this.tokens = tokens;
98100
this.multiSourceReader = multiSourceReader;
99-
this.parserOptions = parserOptions == null ? ParserOptions.getDefaultParserOptions() : parserOptions;
101+
this.parserOptions = ofNullable(parserOptions).orElse(ParserOptions.getDefaultParserOptions());
100102
}
101103

102104
public ParserOptions getParserOptions() {
@@ -790,12 +792,12 @@ private void addIgnoredChars(ParserRuleContext ctx, NodeBuilder nodeBuilder) {
790792
}
791793
Token start = ctx.getStart();
792794
int tokenStartIndex = start.getTokenIndex();
793-
List<Token> leftChannel = tokens.getHiddenTokensToLeft(tokenStartIndex, CHANNEL_IGNORED_CHARS);
795+
List<Token> leftChannel = tokens.getHiddenTokensToLeft(tokenStartIndex, CHANNEL_WHITESPACE);
794796
List<IgnoredChar> ignoredCharsLeft = mapTokenToIgnoredChar(leftChannel);
795797

796798
Token stop = ctx.getStop();
797799
int tokenStopIndex = stop.getTokenIndex();
798-
List<Token> rightChannel = tokens.getHiddenTokensToRight(tokenStopIndex, CHANNEL_IGNORED_CHARS);
800+
List<Token> rightChannel = tokens.getHiddenTokensToRight(tokenStopIndex, CHANNEL_WHITESPACE);
799801
List<IgnoredChar> ignoredCharsRight = mapTokenToIgnoredChar(rightChannel);
800802

801803
nodeBuilder.ignoredChars(new IgnoredChars(ignoredCharsLeft, ignoredCharsRight));

src/main/java/graphql/parser/Parser.java

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package graphql.parser;
22

3+
import graphql.Internal;
34
import graphql.PublicApi;
45
import graphql.language.Document;
56
import graphql.language.Node;
@@ -24,6 +25,8 @@
2425
import java.io.Reader;
2526
import java.io.UncheckedIOException;
2627
import java.util.List;
28+
import java.util.Optional;
29+
import java.util.function.BiConsumer;
2730
import java.util.function.BiFunction;
2831

2932
/**
@@ -45,6 +48,11 @@
4548
@PublicApi
4649
public class Parser {
4750

51+
@Internal
52+
public static final int CHANNEL_COMMENTS = 2;
53+
@Internal
54+
public static final int CHANNEL_WHITESPACE = 3;
55+
4856
/**
4957
* Parses a string input into a graphql AST {@link Document}
5058
*
@@ -195,7 +203,16 @@ public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int
195203
}
196204
});
197205

198-
CommonTokenStream tokens = new CommonTokenStream(lexer);
206+
// default in the parser options if they are not set
207+
parserOptions = Optional.ofNullable(parserOptions).orElse(ParserOptions.getDefaultParserOptions());
208+
209+
// this lexer wrapper allows us to stop lexing when too many tokens are in place. This prevents DOS attacks.
210+
int maxTokens = parserOptions.getMaxTokens();
211+
int maxWhitespaceTokens = parserOptions.getMaxWhitespaceTokens();
212+
BiConsumer<Integer, Token> onTooManyTokens = (maxTokenCount, token) -> throwCancelParseIfTooManyTokens(token, maxTokenCount, multiSourceReader);
213+
SafeTokenSource safeTokenSource = new SafeTokenSource(lexer, maxTokens, maxWhitespaceTokens, onTooManyTokens);
214+
215+
CommonTokenStream tokens = new CommonTokenStream(safeTokenSource);
199216

200217
GraphqlParser parser = new GraphqlParser(tokens);
201218
parser.removeErrorListeners();
@@ -245,23 +262,31 @@ private void setupParserListener(MultiSourceReader multiSourceReader, GraphqlPar
245262

246263
@Override
247264
public void visitTerminal(TerminalNode node) {
265+
Token token = node.getSymbol();
248266
count++;
249267
if (count > maxTokens) {
250-
String msg = String.format("More than %d parse tokens have been presented. To prevent Denial Of Service attacks, parsing has been cancelled.", maxTokens);
251-
SourceLocation sourceLocation = null;
252-
String offendingToken = null;
253-
if (node.getSymbol() != null) {
254-
offendingToken = node.getText();
255-
sourceLocation = AntlrHelper.createSourceLocation(multiSourceReader, node.getSymbol().getLine(), node.getSymbol().getCharPositionInLine());
256-
}
257-
258-
throw new ParseCancelledException(msg, sourceLocation, offendingToken);
268+
throwCancelParseIfTooManyTokens(token, maxTokens, multiSourceReader);
259269
}
260270
}
261271
};
262272
parser.addParseListener(listener);
263273
}
264274

275+
private void throwCancelParseIfTooManyTokens(Token token, int maxTokens, MultiSourceReader multiSourceReader) throws ParseCancelledException {
276+
String tokenType = "grammar";
277+
SourceLocation sourceLocation = null;
278+
String offendingToken = null;
279+
if (token != null) {
280+
int channel = token.getChannel();
281+
tokenType = channel == CHANNEL_WHITESPACE ? "whitespace" : (channel == CHANNEL_COMMENTS ? "comments" : "grammar");
282+
283+
offendingToken = token.getText();
284+
sourceLocation = AntlrHelper.createSourceLocation(multiSourceReader, token.getLine(), token.getCharPositionInLine());
285+
}
286+
String msg = String.format("More than %d %s tokens have been presented. To prevent Denial Of Service attacks, parsing has been cancelled.", maxTokens, tokenType);
287+
throw new ParseCancelledException(msg, sourceLocation, offendingToken);
288+
}
289+
265290
/**
266291
* Allows you to override the ANTLR to AST code.
267292
*

0 commit comments

Comments
 (0)