8000 Backported #3112 into 19.x branch (#3133) · graphql-java/graphql-java@1f905d8 · GitHub
[go: up one dir, main page]

Skip to content

Commit 1f905d8

Browse files
authored
Backported #3112 into 19.x branch (#3133)
Merge pull request #3112 from graphql-java/prevent-stackoverflow-in-parser Preventing stack overflow exceptions via limiting the depth of the parser rules # Conflicts: # src/main/java/graphql/parser/Parser.java # src/main/java/graphql/parser/ParserOptions.java # src/main/resources/i18n/Parsing.properties # src/test/groovy/graphql/parser/ParserTest.groovy
1 parent 214d981 commit 1f905d8

File tree

5 files changed

+268
-110
lines changed

5 files changed

+268
-110
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package graphql.parser;
2+
3+
import graphql.Internal;
4+
import graphql.language.SourceLocation;
5+
import org.jetbrains.annotations.NotNull;
6+
import org.jetbrains.annotations.Nullable;
7+
8+
@Internal
9+
public class ParseCancelledTooDeepException extends InvalidSyntaxException {
10+
11+
@Internal
12+
public ParseCancelledTooDeepException(String msg, @Nullable SourceLocation sourceLocation, @Nullable String offendingToken, int maxTokens, @NotNull String tokenType) {
13+
super(sourceLocation, msg, null, offendingToken, null);
14+
}
15+
}

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

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,11 @@ public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int
236236
// this lexer wrapper allows us to stop lexing when too many tokens are in place. This prevents DOS attacks.
237237
int maxTokens = parserOptions.getMaxTokens();
238238
int maxWhitespaceTokens = parserOptions.getMaxWhitespaceTokens();
239-
BiConsumer<Integer, Token> onTooManyTokens = (maxTokenCount, token) -> throwCancelParseIfTooManyTokens(token, maxTokenCount, multiSourceReader);
239+
BiConsumer<Integer, Token> onTooManyTokens = (maxTokenCount, token) -> throwIfTokenProblems(
240+
token,
241+
maxTokenCount,
242+
multiSourceReader,
243+
ParseCancelledException.class);
240244
SafeTokenSource safeTokenSource = new SafeTokenSource(lexer, maxTokens, maxWhitespaceTokens, onTooManyTokens);
241245

242246
CommonTokenStream tokens = new CommonTokenStream(safeTokenSource);
@@ -285,9 +289,30 @@ private void setupParserListener(MultiSourceReader multiSourceReader, GraphqlPar
285289
ParserOptions parserOptions = toLanguage.getParserOptions();
286290
ParsingListener parsingListener = parserOptions.getParsingListener();
287291
int maxTokens = parserOptions.getMaxTokens();
292+
int maxRuleDepth = parserOptions.getMaxRuleDepth();
288293
// prevent a billion laugh attacks by restricting how many tokens we allow
289294
ParseTreeListener listener = new GraphqlBaseListener() {
290295
int count = 0;
296+
int depth = 0;
297+
298+
299+
@Override
300+
public void enterEveryRule(ParserRuleContext ctx) {
301+
depth++;
302+
if (depth > maxRuleDepth) {
303+
throwIfTokenProblems(
304+
ctx.getStart(),
305+
maxRuleDepth,
306+
multiSourceReader,
307+
ParseCancelledTooDeepException.class
308+
);
309+
}
310+
}
311+
312+
@Override
313+
public void exitEveryRule(ParserRuleContext ctx) {
314+
depth--;
315+
}
291316

292317
@Override
293318
public void visitTerminal(TerminalNode node) {
@@ -312,15 +337,20 @@ public int getCharPositionInLine() {
312337

313338
count++;
314339
if (count > maxTokens) {
315-
throwCancelParseIfTooManyTokens(token, maxTokens, multiSourceReader);
340+
throwIfTokenProblems(
341+
token,
342+
maxTokens,
343+
multiSourceReader,
344+
ParseCancelledException.class
345+
);
316346
}
317347
}
318348
};
319349
parser.addParseListener(listener);
320350
}
321351

322-
private void throwCancelParseIfTooManyTokens(Token token, int maxTokens, MultiSourceReader multiSourceReader) throws ParseCancelledException {
323-
String tokenType = "grammar";
352+
private void throwIfTokenProblems(Token token, int maxLimit, MultiSourceReader multiSourceReader, Class<? extends InvalidSyntaxException> targetException) throws ParseCancelledException {
353+
String tokenType = "grammar";
324354
SourceLocation sourceLocation = null;
325355
String offendingToken = null;
326356
if (token != null) {
@@ -330,9 +360,12 @@ private void throwCancelParseIfTooManyTokens(Token token, int maxTokens, MultiSo
330360
offendingToken = token.getText();
331361
sourceLocation = AntlrHelper.createSourceLocation(multiSourceReader, token.getLine(), token.getCharPositionInLine());
332362
}
333-
String msg = String.format("More than %d %s tokens have been presented. To prevent Denial Of Service attacks, parsing has been cancelled.", maxTokens, tokenType);
334-
throw new ParseCancelledException(msg, sourceLocation, offendingToken);
335-
}
363+
if (targetException.equals(ParseCancelledTooDeepException.class)) {
364+
String msg = String.format("More than %d deep %s rules have been entered. To prevent Denial Of Service attacks, parsing has been cancelled.", maxLimit, tokenType);
365+
throw new ParseCancelledTooDeepException(msg, sourceLocation, offendingToken, maxLimit, tokenType);
366+
}
367+
String msg = String.format("More than %d %s tokens have been presented. To prevent Denial Of Service attacks, parsing has been cancelled.", maxLimit, tokenType);
368+
throw new ParseCancelledException(msg, sourceLocation, offendingToken); }
336369

337370
/**
338371
* Allows you to override the ANTLR to AST code.

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-
*
< 4D1C /td>
20+
* <p>
2121
* If you want to allow more, then {@link #setDefaultParserOptions(ParserOptions)} allows you to change this
2222
* JVM wide.
2323
*/
@@ -26,18 +26,29 @@ 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 500 grammar rules deep.
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 = 500;
44+
3545
private static ParserOptions defaultJvmParserOptions = newParserOptions()
3646
.captureIgnoredChars(false)
3747
.captureSourceLocation(true)
3848
.captureLineComments(true)
3949
.maxTokens(MAX_QUERY_TOKENS) // to prevent a billion laughs style attacks, we set a default for graphql-java
4050
.maxWhitespaceTokens(MAX_WHITESPACE_TOKENS)
51+
.maxRuleDepth(MAX_RULE_DEPTH)
4152
.build();
4253

4354
private static ParserOptions defaultJvmOperationParserOptions = newParserOptions()
@@ -46,6 +57,7 @@ public class ParserOptions {
4657
.captureLineComments(false) // #comments are not useful in query parsing
4758
.maxTokens(MAX_QUERY_TOKENS) // to prevent a billion laughs style attacks, we set a default for graphql-java
4859
.maxWhitespaceTokens(MAX_WHITESPACE_TOKENS)
60+
.maxRuleDepth(MAX_RULE_DEPTH)
4961
.build();
5062

5163
private static ParserOptions defaultJvmSdlParserOptions = newParserOptions()
@@ -54,6 +66,7 @@ public class ParserOptions {
5466
.captureLineComments(true) // #comments are useful in SDL parsing
5567
.maxTokens(Integer.MAX_VALUE) // we are less worried about a billion laughs with SDL parsing since the call path is not facing attackers
5668
.maxWhitespaceTokens(Integer.MAX_VALUE)
69+
.maxRuleDepth(Integer.MAX_VALUE)
5770
.build();
4D1C
5871

5972
/**
@@ -156,6 +169,7 @@ public static void setDefaultSdlParserOptions(ParserOptions options) {
156169
private final boolean captureLineComments;
157170
private final int maxTokens;
158171
private final int maxWhitespaceTokens;
172+
private final int maxRuleDepth;
159173
private final ParsingListener parsingListener;
160174

161175
private ParserOptions(Builder builder) {
@@ -164,6 +178,7 @@ private ParserOptions(Builder builder) {
164178
this.captureLineComments = builder.captureLineComments;
165179
this.maxTokens = builder.maxTokens;
166180
this.maxWhitespaceTokens = builder.maxWhitespaceTokens;
181+
this.maxRuleDepth = builder.maxRuleDepth;
167182
this.parsingListener = builder.parsingListener;
168183
}
169184

@@ -226,6 +241,17 @@ public int getMaxWhitespaceTokens() {
226241
return maxWhitespaceTokens;
227242
}
228243

244+
/**
245+
* A graphql hacking vector is to send nonsensical queries that have lots of rule depth to them which
246+
* can cause stack overflow exceptions during the query parsing. To prevent this you can set a value
247+
* that is the maximum depth allowed before an exception is thrown and the parsing is stopped.
248+
*
249+
* @return the maximum token depth the parser will accept, after which an exception will be thrown.
250+
*/
251+
public int getMaxRuleDepth() {
252+
return maxRuleDepth;
253+
}
254+
229255
public ParsingListener getParsingListener() {
230256
return parsingListener;
231257
}
@@ -245,9 +271,10 @@ public static class Builder {
245271
private boolean captureIgnoredChars = false;
246272
private boolean captureSourceLocation = true;
247273
private boolean captureLineComments = true;
248-
private int maxTokens = MAX_QUERY_TOKENS;
249274
private ParsingListener parsingListener = ParsingListener.NOOP;
275+
private int maxTokens = MAX_QUERY_TOKENS;
250276
private int maxWhitespaceTokens = MAX_WHITESPACE_TOKENS;
277+
private int maxRuleDepth = MAX_RULE_DEPTH;
251278

252279
Builder() {
253280
}
@@ -258,6 +285,7 @@ public static class Builder {
258285
this.captureLineComments = parserOptions.captureLineComments;
259286
this.maxTokens = parserOptions.maxTokens;
260287
this.maxWhitespaceTokens = parserOptions.maxWhitespaceTokens;
288+
this.maxRuleDepth = parserOptions.maxRuleDepth;
261289
this.parsingListener = parserOptions.parsingListener;
262290
}
263291

@@ -286,6 +314,11 @@ public Builder maxWhitespaceTokens(int maxWhitespaceTokens) {
286314
return this;
287315
}
288316

317+
public Builder maxRuleDepth(int maxRuleDepth) {
318+
this.maxRuleDepth = maxRuleDepth;
319+
return this;
320+
}
321+
289322
public Builder parsingListener(ParsingListener parsingListener) {
290323
this.parsingListener = assertNotNull(parsingListener);
291324
return this;

0 commit comments

Comments
 (0)
0