Skip to content

Commit 7a4b8de

Browse files
authored
This is the backport of the max characters in a parse to the 19.x branch (#3158)
1 parent 1f905d8 commit 7a4b8de

7 files changed

Lines changed: 277 additions & 40 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package graphql.parser;
2+
3+
import graphql.Internal;
4+
5+
@Internal
6+
public class ParseCancelledTooManyCharsException extends InvalidSyntaxException {
7+
8+
@Internal
9+
public ParseCancelledTooManyCharsException(String msg, int maxCharacters) {
10+
super(null, msg, null, null, null);
11+
}
12+
}

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

Lines changed: 73 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.antlr.v4.runtime.atn.PredictionMode;
2222
import org.antlr.v4.runtime.tree.ParseTreeListener;
2323
import org.antlr.v4.runtime.tree.TerminalNode;
24+
import org.jetbrains.annotations.NotNull;
2425

2526
import java.io.IOException;
2627
import java.io.Reader;
@@ -29,17 +30,18 @@
2930
import java.util.Optional;
3031
import java.util.function.BiConsumer;
3132
import java.util.function.BiFunction;
33+
import java.util.function.Consumer;
3234

3335
/**
3436
* This can parse graphql syntax, both Query syntax and Schema Definition Language (SDL) syntax, into an
3537
* Abstract Syntax Tree (AST) represented by a {@link Document}
3638
* <p>
3739
* You should not generally need to call this class as the {@link graphql.GraphQL} code sets this up for you
3840
* but if you are doing specific graphql utilities this class is essential.
39-
*
41+
* <p>
4042
* Graphql syntax has a series of characters, such as spaces, new lines and commas that are not considered relevant
4143
* to the syntax. However they can be captured and associated with the AST elements they belong to.
42-
*
44+
* <p>
4345
* This costs more memory but for certain use cases (like editors) this maybe be useful. We have chosen to no capture
4446
* ignored characters by default but you can turn this on, either per parse or statically for the whole JVM
4547
* via {@link ParserOptions#setDefaultParserOptions(ParserOptions)} ()}}
@@ -205,43 +207,18 @@ private Type<?> parseTypeImpl(String input) throws InvalidSyntaxException {
205207
}
206208

207209
private Node<?> parseImpl(Reader reader, BiFunction<GraphqlParser, GraphqlAntlrToLanguage, Object[]> nodeFunction, ParserOptions parserOptions) throws InvalidSyntaxException {
208-
MultiSourceReader multiSourceReader;
209-
if (reader instanceof MultiSourceReader) {
210-
multiSourceReader = (MultiSourceReader) reader;
211-
} else {
212-
multiSourceReader = MultiSourceReader.newMultiSourceReader()
213-
.reader(reader, null).build();
214-
}
215-
CodePointCharStream charStream;
216-
try {
217-
charStream = CharStreams.fromReader(multiSourceReader);
218-
} catch (IOException e) {
219-
throw new UncheckedIOException(e);
220-
}
210+
parserOptions = Optional.ofNullable(parserOptions).orElse(ParserOptions.getDefaultParserOptions());
221211

222-
GraphqlLexer lexer = new GraphqlLexer(charStream);
223-
lexer.removeErrorListeners();
224-
lexer.addErrorListener(new BaseErrorListener() {
225-
@Override
226-
public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) {
227-
SourceLocation sourceLocation = AntlrHelper.createSourceLocation(multiSourceReader, line, charPositionInLine);
228-
String preview = AntlrHelper.createPreview(multiSourceReader, line);
229-
throw new InvalidSyntaxException(sourceLocation, msg, preview, null, null);
230-
}
231-
});
212+
MultiSourceReader multiSourceReader = setupMultiSourceReader(reader, parserOptions);
232213

233-
// default in the parser options if they are not set
234-
parserOptions = Optional.ofNullable(parserOptions).orElse(ParserOptions.getDefaultParserOptions());
214+
SafeTokenReader safeTokenReader = setupSafeTokenReader(parserOptions, multiSourceReader);
215+
216+
CodePointCharStream charStream = setupCharStream(safeTokenReader);
217+
218+
GraphqlLexer lexer = setupGraphqlLexer(multiSourceReader, charStream);
235219

236220
// this lexer wrapper allows us to stop lexing when too many tokens are in place. This prevents DOS attacks.
237-
int maxTokens = parserOptions.getMaxTokens();
238-
int maxWhitespaceTokens = parserOptions.getMaxWhitespaceTokens();
239-
BiConsumer<Integer, Token> onTooManyTokens = (maxTokenCount, token) -> throwIfTokenProblems(
240-
token,
241-
maxTokenCount,
242-
multiSourceReader,
243-
ParseCancelledException.class);
244-
SafeTokenSource safeTokenSource = new SafeTokenSource(lexer, maxTokens, maxWhitespaceTokens, onTooManyTokens);
221+
SafeTokenSource safeTokenSource = getSafeTokenSource(parserOptions, multiSourceReader, lexer);
245222

246223
CommonTokenStream tokens = new CommonTokenStream(safeTokenSource);
247224

@@ -285,6 +262,65 @@ public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int
285262
return node;
286263
}
287264

265+
private static MultiSourceReader setupMultiSourceReader(Reader reader, ParserOptions parserOptions) {
266+
MultiSourceReader multiSourceReader;
267+
if (reader instanceof MultiSourceReader) {
268+
multiSourceReader = (MultiSourceReader) reader;
269+
} else {
270+
multiSourceReader = MultiSourceReader.newMultiSourceReader()
271+
.reader(reader, null).build();
272+
}
273+
return multiSourceReader;
274+
}
275+
276+
@NotNull
277+
private static SafeTokenReader setupSafeTokenReader(ParserOptions parserOptions, MultiSourceReader multiSourceReader) {
278+
int maxCharacters = parserOptions.getMaxCharacters();
279+
Consumer<Integer> onTooManyCharacters = it -> {
280+
String msg = String.format("More than %d characters have been presented. To prevent Denial Of Service attacks, parsing has been cancelled.", maxCharacters);
281+
throw new ParseCancelledTooManyCharsException(msg, maxCharacters);
282+
};
283+
return new SafeTokenReader(multiSourceReader, maxCharacters, onTooManyCharacters);
284+
}
285+
286+
@NotNull
287+
private static CodePointCharStream setupCharStream(SafeTokenReader safeTokenReader) {
288+
CodePointCharStream charStream;
289+
try {
290+
charStream = CharStreams.fromReader(safeTokenReader);
291+
} catch (IOException e) {
292+
throw new UncheckedIOException(e);
293+
}
294+
return charStream;
295+
}
296+
297+
@NotNull
298+
private static GraphqlLexer setupGraphqlLexer(MultiSourceReader multiSourceReader, CodePointCharStream charStream) {
299+
GraphqlLexer lexer = new GraphqlLexer(charStream);
300+
lexer.removeErrorListeners();
301+
lexer.addErrorListener(new BaseErrorListener() {
302+
@Override
303+
public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) {
304+
SourceLocation sourceLocation = AntlrHelper.createSourceLocation(multiSourceReader, line, charPositionInLine);
305+
String preview = AntlrHelper.createPreview(multiSourceReader, line);
306+
throw new InvalidSyntaxException(sourceLocation, msg, preview, null, null);
307+
}
308+
});
309+
return lexer;
310+
}
311+
312+
@NotNull
313+
private SafeTokenSource getSafeTokenSource(ParserOptions parserOptions, MultiSourceReader multiSourceReader, GraphqlLexer lexer) {
314+
int maxTokens = parserOptions.getMaxTokens();
315+
int maxWhitespaceTokens = parserOptions.getMaxWhitespaceTokens();
316+
BiConsumer<Integer, Token> onTooManyTokens = (maxTokenCount, token) -> throwIfTokenProblems(
317+
token,
318+
maxTokenCount,
319+
multiSourceReader,
320+
ParseCancelledException.class);
321+
return new SafeTokenSource(lexer, maxTokens, maxWhitespaceTokens, onTooManyTokens);
322+
}
323+
288324
private void setupParserListener(MultiSourceReader multiSourceReader, GraphqlParser parser, GraphqlAntlrToLanguage toLanguage) {
289325
ParserOptions parserOptions = toLanguage.getParserOptions();
290326
ParsingListener parsingListener = parserOptions.getParsingListener();
@@ -365,7 +401,8 @@ private void throwIfTokenProblems(Token token, int maxLimit, MultiSourceReader m
365401
throw new ParseCancelledTooDeepException(msg, sourceLocation, offendingToken, maxLimit, tokenType);
366402
}
367403
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); }
404+
throw new ParseCancelledException(msg, sourceLocation, offendingToken);
405+
}
369406

370407
/**
371408
* Allows you to override the ANTLR to AST code.

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

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,20 @@
1111
*/
1212
@PublicApi
1313
public class ParserOptions {
14+
/**
15+
* A graphql hacking vector is to send nonsensical queries with large tokens that contain a repeated characters
16+
* that burn lots of parsing CPU time and burn memory representing a document that won't ever execute.
17+
* To prevent this for most users, graphql-java sets this value to 1MB.
18+
* ANTLR parsing time is linear to the number of characters presented. The more you
19+
* allow the longer it takes.
20+
* <p>
21+
* If you want to allow more, then {@link #setDefaultParserOptions(ParserOptions)} allows you to change this
22+
* JVM wide.
23+
*/
24+
public static final int MAX_QUERY_CHARACTERS = 1024 * 1024; // 1 MB
1425

1526
/**
16-
* A graphql hacking vector is to send nonsensical queries that burn lots of parsing CPU time and burn
27+
* A graphql hacking vector is to send nonsensical queries with lots of tokens that burn lots of parsing CPU time and burn
1728
* memory representing a document that won't ever execute. To prevent this for most users, graphql-java
1829
* sets this value to 15000. ANTLR parsing time is linear to the number of tokens presented. The more you
1930
* allow the longer it takes.
@@ -46,6 +57,7 @@ public class ParserOptions {
4657
.captureIgnoredChars(false)
4758
.captureSourceLocation(true)
4859
.captureLineComments(true)
60+
.maxCharacters(MAX_QUERY_CHARACTERS)
4961
.maxTokens(MAX_QUERY_TOKENS) // to prevent a billion laughs style attacks, we set a default for graphql-java
5062
.maxWhitespaceTokens(MAX_WHITESPACE_TOKENS)
5163
.maxRuleDepth(MAX_RULE_DEPTH)
@@ -55,6 +67,7 @@ public class ParserOptions {
5567
.captureIgnoredChars(false)
5668
.captureSourceLocation(true)
5769
.captureLineComments(false) // #comments are not useful in query parsing
70+
.maxCharacters(MAX_QUERY_CHARACTERS)
5871
.maxTokens(MAX_QUERY_TOKENS) // to prevent a billion laughs style attacks, we set a default for graphql-java
5972
.maxWhitespaceTokens(MAX_WHITESPACE_TOKENS)
6073
.maxRuleDepth(MAX_RULE_DEPTH)
@@ -64,6 +77,7 @@ public class ParserOptions {
6477
.captureIgnoredChars(false)
6578
.captureSourceLocation(true)
6679
.captureLineComments(true) // #comments are useful in SDL parsing
80+
.maxCharacters(Integer.MAX_VALUE)
6781
.maxTokens(Integer.MAX_VALUE) // we are less worried about a billion laughs with SDL parsing since the call path is not facing attackers
6882
.maxWhitespaceTokens(Integer.MAX_VALUE)
6983
.maxRuleDepth(Integer.MAX_VALUE)
@@ -167,6 +181,7 @@ public static void setDefaultSdlParserOptions(ParserOptions options) {
167181
private final boolean captureIgnoredChars;
168182
private final boolean captureSourceLocation;
169183
private final boolean captureLineComments;
184+
private final int maxCharacters;
170185
private final int maxTokens;
171186
private final int maxWhitespaceTokens;
172187
private final int maxRuleDepth;
@@ -176,6 +191,7 @@ private ParserOptions(Builder builder) {
176191
this.captureIgnoredChars = builder.captureIgnoredChars;
177192
this.captureSourceLocation = builder.captureSourceLocation;
178193
this.captureLineComments = builder.captureLineComments;
194+
this.maxCharacters = builder.maxCharacters;
179195
this.maxTokens = builder.maxTokens;
180196
this.maxWhitespaceTokens = builder.maxWhitespaceTokens;
181197
this.maxRuleDepth = builder.maxRuleDepth;
@@ -219,6 +235,18 @@ public boolean isCaptureLineComments() {
219235
return captureLineComments;
220236
}
221237

238+
/**
239+
* A graphql hacking vector is to send nonsensical queries that contain a repeated characters that burn lots of parsing CPU time and burn
240+
* memory representing a document that won't ever execute. To prevent this for most users, graphql-java
241+
* sets this value to 1MB.
242+
*
243+
* @return the maximum number of characters the parser will accept, after which an exception will be thrown.
244+
*/
245+
public int getMaxCharacters() {
246+
return maxCharacters;
247+
}
248+
249+
222250
/**
223251
* A graphql hacking vector is to send nonsensical queries that burn lots of parsing CPU time and burns
224252
* memory representing a document that won't ever execute. To prevent this you can set a maximum number of parse
@@ -272,6 +300,7 @@ public static class Builder {
272300
private boolean captureSourceLocation = true;
273301
private boolean captureLineComments = true;
274302
private ParsingListener parsingListener = ParsingListener.NOOP;
303+
private int maxCharacters = MAX_QUERY_CHARACTERS;
275304
private int maxTokens = MAX_QUERY_TOKENS;
276305
private int maxWhitespaceTokens = MAX_WHITESPACE_TOKENS;
277306
private int maxRuleDepth = MAX_RULE_DEPTH;
@@ -283,6 +312,7 @@ public static class Builder {
283312
this.captureIgnoredChars = parserOptions.captureIgnoredChars;
284313
this.captureSourceLocation = parserOptions.captureSourceLocation;
285314
this.captureLineComments = parserOptions.captureLineComments;
315+
this.maxCharacters = parserOptions.maxCharacters;
286316
this.maxTokens = parserOptions.maxTokens;
287317
this.maxWhitespaceTokens = parserOptions.maxWhitespaceTokens;
288318
this.maxRuleDepth = parserOptions.maxRuleDepth;
@@ -304,6 +334,11 @@ public Builder captureLineComments(boolean captureLineComments) {
304334
return this;
305335
}
306336

337+
public Builder maxCharacters(int maxCharacters) {
338+
this.maxCharacters = maxCharacters;
339+
return this;
340+
}
341+
307342
public Builder maxTokens(int maxTokens) {
308343
this.maxTokens = maxTokens;
309344
return this;
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package graphql.parser;
2+
3+
import graphql.Internal;
4+
import org.jetbrains.annotations.NotNull;
5+
6+
import java.io.IOException;
7+
import java.io.Reader;
8+
import java.nio.CharBuffer;
9+
import java.util.function.Consumer;
10+
11+
/**
12+
* This reader will only emit a maximum number of characters from it. This is used to protect us from evil input.
13+
* <p>
14+
* If a graphql system does not have some max HTTP input limit, then this will help protect the system. This is a limit
15+
* of last resort. Ideally the http input should be limited, but if its not, we have this.
16+
*/
17+
@Internal
18+
public class SafeTokenReader extends Reader {
19+
20+
private final Reader delegate;
21+
private final int maxCharacters;
22+
private final Consumer<Integer> whenMaxCharactersExceeded;
23+
private int count;
24+
25+
public SafeTokenReader(Reader delegate, int maxCharacters, Consumer<Integer> whenMaxCharactersExceeded) {
26+
this.delegate = delegate;
27+
this.maxCharacters = maxCharacters;
28+
this.whenMaxCharactersExceeded = whenMaxCharactersExceeded;
29+
count = 0;
30+
}
31+
32+
private int checkHowMany(int read, int howMany) {
33+
if (read != -1) {
34+
count += howMany;
35+
if (count > maxCharacters) {
36+
whenMaxCharactersExceeded.accept(maxCharacters);
37+
}
38+
}
39+
return read;
40+
}
41+
42+
@Override
43+
public int read(char @NotNull [] buff, int off, int len) throws IOException {
44+
int howMany = delegate.read(buff, off, len);
45+
return checkHowMany(howMany, howMany);
46+
}
47+
48+
@Override
49+
public int read() throws IOException {
50+
int ch = delegate.read();
51+
return checkHowMany(ch, 1);
52+
}
53+
54+
@Override
55+
public int read(@NotNull CharBuffer target) throws IOException {
56+
int howMany = delegate.read(target);
57+
return checkHowMany(howMany, howMany);
58+
}
59+
60+
@Override
61+
public int read( char @NotNull [] buff) throws IOException {
62+
int howMany = delegate.read(buff);
63+
return checkHowMany(howMany, howMany);
64+
}
65+
66+
@Override
67+
public void close() throws IOException {
68+
delegate.close();
69+
}
70+
71+
@Override
72+
public long skip(long n) throws IOException {
73+
return delegate.skip(n);
74+
}
75+
76+
@Override
77+
public boolean ready() throws IOException {
78+
return delegate.ready();
79+
}
80+
81+
@Override
82+
public boolean markSupported() {
83+
return delegate.markSupported();
84+
}
85+
86+
@Override
87+
public void mark(int readAheadLimit) throws IOException {
88+
delegate.mark(readAheadLimit);
89+
}
90+
91+
@Override
92+
public void reset() throws IOException {
93+
delegate.reset();
94+
}
95+
}

0 commit comments

Comments
 (0)