Skip to content

Commit 8a1c884

Browse files
committed
Preventing stack overflow exceptions via limiting the depth of the parser rules
1 parent 4c66935 commit 8a1c884

6 files changed

Lines changed: 297 additions & 137 deletions

File tree

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

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import graphql.parser.antlr.GraphqlLexer;
1414
import graphql.parser.antlr.GraphqlParser;
1515
import graphql.parser.exceptions.ParseCancelledException;
16+
import graphql.parser.exceptions.ParseCancelledTooDeepException;
1617
import org.antlr.v4.runtime.BaseErrorListener;
1718
import org.antlr.v4.runtime.CharStreams;
1819
import org.antlr.v4.runtime.CodePointCharStream;
@@ -299,7 +300,12 @@ public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int
299300
// this lexer wrapper allows us to stop lexing when too many tokens are in place. This prevents DOS attacks.
300301
int maxTokens = parserOptions.getMaxTokens();
301302
int maxWhitespaceTokens = parserOptions.getMaxWhitespaceTokens();
302-
BiConsumer<Integer, Token> onTooManyTokens = (maxTokenCount, token) -> throwCancelParseIfTooManyTokens(environment, token, maxTokenCount, multiSourceReader);
303+
BiConsumer<Integer, Token> onTooManyTokens = (maxTokenCount, token) -> throwIfTokenProblems(
304+
environment,
305+
token,
306+
maxTokenCount,
307+
multiSourceReader,
308+
ParseCancelledException.class);
303309
SafeTokenSource safeTokenSource = new SafeTokenSource(lexer, maxTokens, maxWhitespaceTokens, onTooManyTokens);
304310

305311
CommonTokenStream tokens = new CommonTokenStream(safeTokenSource);
@@ -345,9 +351,31 @@ private void setupParserListener(ParserEnvironment environment, MultiSourceReade
345351
ParserOptions parserOptions = toLanguage.getParserOptions();
346352
ParsingListener parsingListener = parserOptions.getParsingListener();
347353
int maxTokens = parserOptions.getMaxTokens();
354+
int maxRuleDepth = parserOptions.getMaxRuleDepth();
348355
// prevent a billion laugh attacks by restricting how many tokens we allow
349356
ParseTreeListener listener = new GraphqlBaseListener() {
350357
int count = 0;
358+
int depth = 0;
359+
360+
361+
@Override
362+
public void enterEveryRule(ParserRuleContext ctx) {
363+
depth++;
364+
if (depth > maxRuleDepth) {
365+
throwIfTokenProblems(
366+
environment,
367+
ctx.getStart(),
368+
maxRuleDepth,
369+
multiSourceReader,
370+
ParseCancelledTooDeepException.class
371+
);
372+
}
373+
}
374+
375+
@Override
376+
public void exitEveryRule(ParserRuleContext ctx) {
377+
depth--;
378+
}
351379

352380
@Override
353381
public void visitTerminal(TerminalNode node) {
@@ -372,14 +400,20 @@ public int getCharPositionInLine() {
372400

373401
count++;
374402
if (count > maxTokens) {
375-
throwCancelParseIfTooManyTokens(environment, token, maxTokens, multiSourceReader);
403+
throwIfTokenProblems(
404+
environment,
405+
token,
406+
maxTokens,
407+
multiSourceReader,
408+
ParseCancelledException.class
409+
);
376410
}
377411
}
378412
};
379413
parser.addParseListener(listener);
380414
}
381415

382-
private void throwCancelParseIfTooManyTokens(ParserEnvironment environment, Token token, int maxTokens, MultiSourceReader multiSourceReader) throws ParseCancelledException {
416+
private void throwIfTokenProblems(ParserEnvironment environment, Token token, int maxLimit, MultiSourceReader multiSourceReader, Class<? extends InvalidSyntaxException> targetException) throws ParseCancelledException {
383417
String tokenType = "grammar";
384418
SourceLocation sourceLocation = null;
385419
String offendingToken = null;
@@ -390,7 +424,10 @@ private void throwCancelParseIfTooManyTokens(ParserEnvironment environment, Toke
390424
offendingToken = token.getText();
391425
sourceLocation = AntlrHelper.createSourceLocation(multiSourceReader, token.getLine(), token.getCharPositionInLine());
392426
}
393-
throw new ParseCancelledException(environment.getI18N(), sourceLocation, offendingToken, maxTokens, tokenType);
427+
if (targetException.equals(ParseCancelledTooDeepException.class)) {
428+
throw new ParseCancelledTooDeepException(environment.getI18N(), sourceLocation, offendingToken, maxLimit, tokenType);
429+
}
430+
throw new ParseCancelledException(environment.getI18N(), sourceLocation, offendingToken, maxLimit, tokenType);
394431
}
395432

396433
/**

src/main/java/graphql/parser/ParserOptions.java

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ public class ParserOptions {
1515
/**
1616
* A graphql hacking vector is to send nonsensical queries that burn lots of parsing CPU time and burn
1717
* memory representing a document that won't ever execute. To prevent this for most users, graphql-java
18-
* set this value to 15000. ANTLR parsing time is linear to the number of tokens presented. The more you
18+
* sets this value to 15000. ANTLR parsing time is linear to the number of tokens presented. The more you
1919
* allow the longer it takes.
20-
*
20+
* <p>
2121
* If you want to allow more, then {@link #setDefaultParserOptions(ParserOptions)} allows you to change this
2222
* JVM wide.
2323
*/
@@ -26,19 +26,30 @@ public class ParserOptions {
2626
* Another graphql hacking vector is to send large amounts of whitespace in operations that burn lots of parsing CPU time and burn
2727
* memory representing a document. Whitespace token processing in ANTLR is 2 orders of magnitude faster than grammar token processing
2828
* however it still takes some time to happen.
29-
*
29+
* <p>
3030
* If you want to allow more, then {@link #setDefaultParserOptions(ParserOptions)} allows you to change this
3131
* JVM wide.
3232
*/
3333
public static final int MAX_WHITESPACE_TOKENS = 200_000;
3434

35+
/**
36+
* A graphql hacking vector is to send nonsensical queries that have lots of grammar rule depth to them which
37+
* can cause stack overflow exceptions during the query parsing. To prevent this for most users, graphql-java
38+
* sets this value to 1000.
39+
* <p>
40+
* If you want to allow more, then {@link #setDefaultParserOptions(ParserOptions)} allows you to change this
41+
* JVM wide.
42+
*/
43+
public static final int MAX_RULE_DEPTH = 1_000;
44+
3545
private static ParserOptions defaultJvmParserOptions = newParserOptions()
3646
.captureIgnoredChars(false)
3747
.captureSourceLocation(true)
3848
.captureLineComments(true)
3949
.readerTrackData(true)
4050
.maxTokens(MAX_QUERY_TOKENS) // to prevent a billion laughs style attacks, we set a default for graphql-java
4151
.maxWhitespaceTokens(MAX_WHITESPACE_TOKENS)
52+
.maxRuleDepth(MAX_RULE_DEPTH)
4253
.build();
4354

4455
private static ParserOptions defaultJvmOperationParserOptions = newParserOptions()
@@ -48,6 +59,7 @@ public class ParserOptions {
4859
.readerTrackData(true)
4960
.maxTokens(MAX_QUERY_TOKENS) // to prevent a billion laughs style attacks, we set a default for graphql-java
5061
.maxWhitespaceTokens(MAX_WHITESPACE_TOKENS)
62+
.maxRuleDepth(MAX_RULE_DEPTH)
5163
.build();
5264

5365
private static ParserOptions defaultJvmSdlParserOptions = newParserOptions()
@@ -57,6 +69,7 @@ public class ParserOptions {
5769
.readerTrackData(true)
5870
.maxTokens(Integer.MAX_VALUE) // we are less worried about a billion laughs with SDL parsing since the call path is not facing attackers
5971
.maxWhitespaceTokens(Integer.MAX_VALUE)
72+
.maxRuleDepth(Integer.MAX_VALUE)
6073
.build();
6174

6275
/**
@@ -160,6 +173,7 @@ public static void setDefaultSdlParserOptions(ParserOptions options) {
160173
private final boolean readerTrackData;
161174
private final int maxTokens;
162175
private final int maxWhitespaceTokens;
176+
private final int maxRuleDepth;
163177
private final ParsingListener parsingListener;
164178

165179
private ParserOptions(Builder builder) {
@@ -169,6 +183,7 @@ private ParserOptions(Builder builder) {
169183
this.readerTrackData = builder.readerTrackData;
170184
this.maxTokens = builder.maxTokens;
171185
this.maxWhitespaceTokens = builder.maxWhitespaceTokens;
186+
this.maxRuleDepth = builder.maxRuleDepth;
172187
this.parsingListener = builder.parsingListener;
173188
}
174189

@@ -240,6 +255,17 @@ public int getMaxWhitespaceTokens() {
240255
return maxWhitespaceTokens;
241256
}
242257

258+
/**
259+
* A graphql hacking vector is to send nonsensical queries that have lots of rule depth to them which
260+
* can cause stack overflow exceptions during the query parsing. To prevent this you can set a value
261+
* that is the maximum depth allowed before an exception is thrown and the parsing is stopped.
262+
*
263+
* @return the maximum token depth the parser will accept, after which an exception will be thrown.
264+
*/
265+
public int getMaxRuleDepth() {
266+
return maxRuleDepth;
267+
}
268+
243269
public ParsingListener getParsingListener() {
244270
return parsingListener;
245271
}
@@ -260,9 +286,10 @@ public static class Builder {
260286
private boolean captureSourceLocation = true;
261287
private boolean captureLineComments = true;
262288
private boolean readerTrackData = true;
263-
private int maxTokens = MAX_QUERY_TOKENS;
264289
private ParsingListener parsingListener = ParsingListener.NOOP;
290+
private int maxTokens = MAX_QUERY_TOKENS;
265291
private int maxWhitespaceTokens = MAX_WHITESPACE_TOKENS;
292+
private int maxRuleDepth = MAX_RULE_DEPTH;
266293

267294
Builder() {
268295
}
@@ -273,6 +300,7 @@ public static class Builder {
273300
this.captureLineComments = parserOptions.captureLineComments;
274301
this.maxTokens = parserOptions.maxTokens;
275302
this.maxWhitespaceTokens = parserOptions.maxWhitespaceTokens;
303+
this.maxRuleDepth = parserOptions.maxRuleDepth;
276304
this.parsingListener = parserOptions.parsingListener;
277305
}
278306

@@ -306,6 +334,11 @@ public Builder maxWhitespaceTokens(int maxWhitespaceTokens) {
306334
return this;
307335
}
308336

337+
public Builder maxRuleDepth(int maxRuleDepth) {
338+
this.maxRuleDepth = maxRuleDepth;
339+
return this;
340+
}
341+
309342
public Builder parsingListener(ParsingListener parsingListener) {
310343
this.parsingListener = assertNotNull(parsingListener);
311344
return this;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package graphql.parser.exceptions;
2+
3+
import graphql.Internal;
4+
import graphql.i18n.I18n;
5+
import graphql.language.SourceLocation;
6+
import graphql.parser.InvalidSyntaxException;
7+
import org.jetbrains.annotations.NotNull;
8+
import org.jetbrains.annotations.Nullable;
9+
10+
@Internal
11+
public class ParseCancelledTooDeepException extends InvalidSyntaxException {
12+
13+
@Internal
14+
public ParseCancelledTooDeepException(@NotNull I18n i18N, @Nullable SourceLocation sourceLocation, @Nullable String offendingToken, int maxTokens, @NotNull String tokenType) {
15+
super(i18N.msg("ParseCancelled.tooDeep", maxTokens, tokenType),
16+
sourceLocation, offendingToken, null, null);
17+
}
18+
}

src/main/resources/i18n/Parsing.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ InvalidSyntaxBail.full=Invalid syntax with offending token ''{0}'' at line {1} c
1919
InvalidSyntaxMoreTokens.full=Invalid syntax encountered. There are extra tokens in the text that have not been consumed. Offending token ''{0}'' at line {1} column {2}
2020
#
2121
ParseCancelled.full=More than {0} ''{1}'' tokens have been presented. To prevent Denial Of Service attacks, parsing has been cancelled.
22+
ParseCancelled.tooDeep=More than {0} deep ''{1}'' rules have been entered. To prevent Denial Of Service attacks, parsing has been cancelled.
2223
#
2324
InvalidUnicode.trailingLeadingSurrogate=Invalid unicode encountered. Trailing surrogate must be preceded with a leading surrogate. Offending token ''{0}'' at line {1} column {2}
2425
InvalidUnicode.leadingTrailingSurrogate=Invalid unicode encountered. Leading surrogate must be followed by a trailing surrogate. Offending token ''{0}'' at line {1} column {2}

0 commit comments

Comments
 (0)