Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/main/java/graphql/parser/Parser.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import graphql.parser.exceptions.ParseCancelledException;
import graphql.parser.exceptions.ParseCancelledTooDeepException;
import graphql.parser.exceptions.ParseCancelledTooManyCharsException;
import graphql.parser.exceptions.ParseCancelledTooManyNumericLiteralCharactersException;
import org.antlr.v4.runtime.BaseErrorListener;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CodePointCharStream;
Expand Down Expand Up @@ -348,13 +349,25 @@ public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int
private SafeTokenSource getSafeTokenSource(ParserEnvironment environment, ParserOptions parserOptions, MultiSourceReader multiSourceReader, GraphqlLexer lexer) {
int maxTokens = parserOptions.getMaxTokens();
int maxWhitespaceTokens = parserOptions.getMaxWhitespaceTokens();
int maxNumericLiteralCharacters = parserOptions.getMaxNumericLiteralCharacters();
BiConsumer<Integer, Token> onTooManyTokens = (maxTokenCount, token) -> throwIfTokenProblems(
environment,
token,
maxTokenCount,
multiSourceReader,
ParseCancelledException.class);
return new SafeTokenSource(lexer, maxTokens, maxWhitespaceTokens, onTooManyTokens);
BiConsumer<Integer, Token> onTooManyNumericLiteralCharacters = (maxCharacters, token) -> {
SourceLocation sourceLocation = AntlrHelper.createSourceLocation(multiSourceReader, token);
throw new ParseCancelledTooManyNumericLiteralCharactersException(environment.getI18N(), sourceLocation, maxCharacters);
};
return new SafeTokenSource(
lexer,
maxTokens,
maxWhitespaceTokens,
maxNumericLiteralCharacters,
onTooManyTokens,
onTooManyNumericLiteralCharacters
);
}

private void setupParserListener(ParserEnvironment environment, MultiSourceReader multiSourceReader, GraphqlParser parser, GraphqlAntlrToLanguage toLanguage) {
Expand Down
41 changes: 41 additions & 0 deletions src/main/java/graphql/parser/ParserOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ public class ParserOptions {
*/
public static final int MAX_WHITESPACE_TOKENS = 200_000;

/**
* A numeric literal is represented by a single token, regardless of how many characters it contains. Converting
* very large numeric literals into arbitrary precision numbers can consume excessive CPU and memory. To prevent
* this for most users, graphql-java limits numeric literals to 100 characters.
* <p>
* If you want to allow more, then {@link #setDefaultParserOptions(ParserOptions)} allows you to change this
* JVM wide.
*/
public static final int MAX_NUMERIC_LITERAL_CHARACTERS = 100;

/**
* A graphql hacking vector is to send nonsensical queries that have lots of grammar rule depth to them which
* can cause stack overflow exceptions during the query parsing. To prevent this for most users, graphql-java
Expand All @@ -61,6 +71,7 @@ public class ParserOptions {
.maxCharacters(MAX_QUERY_CHARACTERS)
.maxTokens(MAX_QUERY_TOKENS) // to prevent a billion laughs style attacks, we set a default for graphql-java
.maxWhitespaceTokens(MAX_WHITESPACE_TOKENS)
.maxNumericLiteralCharacters(MAX_NUMERIC_LITERAL_CHARACTERS)
.maxRuleDepth(MAX_RULE_DEPTH)
.redactTokenParserErrorMessages(false)
.build();
Expand All @@ -73,6 +84,7 @@ public class ParserOptions {
.maxCharacters(MAX_QUERY_CHARACTERS)
.maxTokens(MAX_QUERY_TOKENS) // to prevent a billion laughs style attacks, we set a default for graphql-java
.maxWhitespaceTokens(MAX_WHITESPACE_TOKENS)
.maxNumericLiteralCharacters(MAX_NUMERIC_LITERAL_CHARACTERS)
.maxRuleDepth(MAX_RULE_DEPTH)
.redactTokenParserErrorMessages(false)
.build();
Expand All @@ -85,6 +97,7 @@ public class ParserOptions {
.maxCharacters(Integer.MAX_VALUE)
.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)
.maxNumericLiteralCharacters(MAX_NUMERIC_LITERAL_CHARACTERS)
.maxRuleDepth(Integer.MAX_VALUE)
.redactTokenParserErrorMessages(false)
.build();
Expand Down Expand Up @@ -191,6 +204,7 @@ public static void setDefaultSdlParserOptions(ParserOptions options) {
private final int maxCharacters;
private final int maxTokens;
private final int maxWhitespaceTokens;
private final int maxNumericLiteralCharacters;
private final int maxRuleDepth;
private final boolean redactTokenParserErrorMessages;
private final ParsingListener parsingListener;
Expand All @@ -203,6 +217,7 @@ private ParserOptions(Builder builder) {
this.maxCharacters = builder.maxCharacters;
this.maxTokens = builder.maxTokens;
this.maxWhitespaceTokens = builder.maxWhitespaceTokens;
this.maxNumericLiteralCharacters = builder.maxNumericLiteralCharacters;
this.maxRuleDepth = builder.maxRuleDepth;
this.redactTokenParserErrorMessages = builder.redactTokenParserErrorMessages;
this.parsingListener = builder.parsingListener;
Expand Down Expand Up @@ -288,6 +303,17 @@ public int getMaxWhitespaceTokens() {
return maxWhitespaceTokens;
}

/**
* A numeric literal is represented by a single token, regardless of how many characters it contains. Converting
* very large numeric literals into arbitrary precision numbers can consume excessive CPU and memory. This limit
* stops parsing before that conversion takes place.
*
* @return the maximum number of characters permitted in an integer or floating-point literal
*/
public int getMaxNumericLiteralCharacters() {
return maxNumericLiteralCharacters;
}

/**
* A graphql hacking vector is to send nonsensical queries that have lots of rule depth to them which
* can cause stack overflow exceptions during the query parsing. To prevent this you can set a value
Expand Down Expand Up @@ -333,6 +359,7 @@ public static class Builder {
private int maxCharacters = MAX_QUERY_CHARACTERS;
private int maxTokens = MAX_QUERY_TOKENS;
private int maxWhitespaceTokens = MAX_WHITESPACE_TOKENS;
private int maxNumericLiteralCharacters = MAX_NUMERIC_LITERAL_CHARACTERS;
private int maxRuleDepth = MAX_RULE_DEPTH;
private boolean redactTokenParserErrorMessages = false;

Expand All @@ -346,6 +373,7 @@ public static class Builder {
this.maxCharacters = parserOptions.maxCharacters;
this.maxTokens = parserOptions.maxTokens;
this.maxWhitespaceTokens = parserOptions.maxWhitespaceTokens;
this.maxNumericLiteralCharacters = parserOptions.maxNumericLiteralCharacters;
this.maxRuleDepth = parserOptions.maxRuleDepth;
this.redactTokenParserErrorMessages = parserOptions.redactTokenParserErrorMessages;
this.parsingListener = parserOptions.parsingListener;
Expand Down Expand Up @@ -386,6 +414,19 @@ public Builder maxWhitespaceTokens(int maxWhitespaceTokens) {
return this;
}

/**
* Sets the maximum number of characters permitted in an integer or floating-point literal. Parsing is
* cancelled before converting a larger literal into an arbitrary precision number.
*
* @param maxNumericLiteralCharacters the maximum number of characters permitted in a numeric literal
*
* @return this builder
*/
public Builder maxNumericLiteralCharacters(int maxNumericLiteralCharacters) {
this.maxNumericLiteralCharacters = maxNumericLiteralCharacters;
return this;
}

public Builder maxRuleDepth(int maxRuleDepth) {
this.maxRuleDepth = maxRuleDepth;
return this;
Expand Down
22 changes: 21 additions & 1 deletion src/main/java/graphql/parser/SafeTokenSource.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package graphql.parser;

import graphql.Internal;
import graphql.parser.antlr.GraphqlLexer;
import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.TokenFactory;
Expand All @@ -25,14 +26,20 @@ public class SafeTokenSource implements TokenSource {
private final TokenSource lexer;
private final int maxTokens;
private final int maxWhitespaceTokens;
private final int maxNumericLiteralCharacters;
private final BiConsumer<Integer, Token> whenMaxTokensExceeded;
private final BiConsumer<Integer, Token> whenMaxNumericLiteralCharactersExceeded;
private final int channelCounts[];

public SafeTokenSource(TokenSource lexer, int maxTokens, int maxWhitespaceTokens, BiConsumer<Integer, Token> whenMaxTokensExceeded) {
public SafeTokenSource(TokenSource lexer, int maxTokens, int maxWhitespaceTokens, int maxNumericLiteralCharacters,
BiConsumer<Integer, Token> whenMaxTokensExceeded,
BiConsumer<Integer, Token> whenMaxNumericLiteralCharactersExceeded) {
this.lexer = lexer;
this.maxTokens = maxTokens;
this.maxWhitespaceTokens = maxWhitespaceTokens;
this.maxNumericLiteralCharacters = maxNumericLiteralCharacters;
this.whenMaxTokensExceeded = whenMaxTokensExceeded;
this.whenMaxNumericLiteralCharactersExceeded = whenMaxNumericLiteralCharactersExceeded;
// this could be a Map<int,int> 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!
Expand All @@ -44,6 +51,7 @@ public SafeTokenSource(TokenSource lexer, int maxTokens, int maxWhitespaceTokens
public Token nextToken() {
Token token = lexer.nextToken();
if (token != null) {
callbackIfNumericLiteralTooLong(token);
int channel = token.getChannel();
int currentCount = ++channelCounts[channel];
if (channel == Parser.CHANNEL_WHITESPACE) {
Expand All @@ -56,6 +64,18 @@ public Token nextToken() {
return token;
}

private void callbackIfNumericLiteralTooLong(Token token) {
int tokenType = token.getType();
if (tokenType != GraphqlLexer.IntValue && tokenType != GraphqlLexer.FloatValue) {
return;
}

int characterCount = token.getStopIndex() - token.getStartIndex() + 1;
if (characterCount > maxNumericLiteralCharacters) {
whenMaxNumericLiteralCharactersExceeded.accept(maxNumericLiteralCharacters, token);
}
}

private void callbackIfMaxExceeded(int maxCount, int currentCount, Token token) {
if (currentCount > maxCount) {
whenMaxTokensExceeded.accept(maxCount, token);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package graphql.parser.exceptions;

import graphql.Internal;
import graphql.i18n.I18n;
import graphql.language.SourceLocation;
import graphql.parser.InvalidSyntaxException;
import org.jspecify.annotations.NonNull;

@Internal
public class ParseCancelledTooManyNumericLiteralCharactersException extends InvalidSyntaxException {

@Internal
public ParseCancelledTooManyNumericLiteralCharactersException(@NonNull I18n i18N, @NonNull SourceLocation sourceLocation, int maxCharacters) {
super(i18N.msg("ParseCancelled.tooManyNumericLiteralCharacters", maxCharacters),
sourceLocation, null, null, null);
}
}
1 change: 1 addition & 0 deletions src/main/resources/i18n/Parsing.properties
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ InvalidSyntaxMoreTokens.full=Invalid syntax encountered. There are extra tokens
ParseCancelled.full=More than {0} ''{1}'' tokens have been presented. To prevent Denial Of Service attacks, parsing has been cancelled.
ParseCancelled.tooDeep=More than {0} deep ''{1}'' rules have been entered. To prevent Denial Of Service attacks, parsing has been cancelled.
ParseCancelled.tooManyChars=More than {0} characters have been presented. To prevent Denial Of Service attacks, parsing has been cancelled.
ParseCancelled.tooManyNumericLiteralCharacters=A numeric literal with more than {0} characters has been presented. To prevent Denial Of Service attacks, parsing has been cancelled.
#
InvalidUnicode.trailingLeadingSurrogate=Invalid unicode encountered. Trailing surrogate must be preceded with a leading surrogate. Offending token ''{0}'' at line {1} column {2}
InvalidUnicode.leadingTrailingSurrogate=Invalid unicode encountered. Leading surrogate must be followed by a trailing surrogate. Offending token ''{0}'' at line {1} column {2}
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/i18n/Parsing_de.properties
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ InvalidSyntaxMoreTokens.full=Es wurde eine ungültige Syntax festgestellt. Es gi
ParseCancelled.full=Es wurden mehr als {0} ''{1}'' Token präsentiert. Um Denial-of-Service-Angriffe zu verhindern, wurde das Parsing abgebrochen.
ParseCancelled.tooDeep=Es wurden mehr als {0} tief ''{1}'' Regeln ausgeführt. Um Denial-of-Service-Angriffe zu verhindern, wurde das Parsing abgebrochen.
ParseCancelled.tooManyChars=Es wurden mehr als {0} Zeichen vorgelegt. Um Denial-of-Service-Angriffe zu verhindern, wurde das Parsing abgebrochen.
ParseCancelled.tooManyNumericLiteralCharacters=Es wurde ein numerisches Literal mit mehr als {0} Zeichen vorgelegt. Um Denial-of-Service-Angriffe zu verhindern, wurde das Parsing abgebrochen.
#
InvalidUnicode.trailingLeadingSurrogate=Ungültiger Unicode gefunden. Trailing surrogate muss ein leading surrogate vorangestellt werden. Ungültiges Token ''{0}'' in Zeile {1} Spalte {2}
InvalidUnicode.leadingTrailingSurrogate=Ungültiger Unicode gefunden. Auf ein leading surrogate muss ein trailing surrogate folgen. Ungültiges Token ''{0}'' in Zeile {1} Spalte {2}
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/i18n/Parsing_nl.properties
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ InvalidSyntaxMoreTokens.full=Ongeldige syntaxis tegengekomen. Er zijn tokens in
ParseCancelled.full=Meer dan {0} ''{1}'' tokens zijn gepresenteerd. Om een DDoS-aanval te voorkomen is het parsen gestopt.
ParseCancelled.tooDeep=Meer dan {0} diep, ''{1}'' regels zijn uitgevoerd. Om een DDoS-aanval te voorkomen is het parsen gestopt.
ParseCancelled.tooManyChars=Meer dan {0} tekens zijn voorgelegd. Om een DDoS-aanval te voorkomen is het parsen gestopt.
ParseCancelled.tooManyNumericLiteralCharacters=Er is een numerieke literal met meer dan {0} tekens aangeboden. Om een DDoS-aanval te voorkomen is het parsen gestopt.
#
InvalidUnicode.trailingLeadingSurrogate=Ongeldige Unicode tegengekomen. Trailing surrogate moet vooropgaan aan een leading surrogate. Ongeldige token ''{0}'' op lijn {1} kolom {2}
InvalidUnicode.leadingTrailingSurrogate=Ongeldige Unicode tegengekomen. Leading surrogate moet voorafgaan aan een trailing surrogate. Ongeldige token ''{0}'' op lijn {1} kolom {2}
Expand Down
7 changes: 7 additions & 0 deletions src/test/groovy/graphql/parser/ParserOptionsTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class ParserOptionsTest extends Specification {
defaultOptions.getMaxCharacters() == ONE_MB
defaultOptions.getMaxTokens() == 15_000
defaultOptions.getMaxWhitespaceTokens() == 200_000
defaultOptions.getMaxNumericLiteralCharacters() == 100
defaultOptions.isCaptureSourceLocation()
defaultOptions.isCaptureLineComments()
!defaultOptions.isCaptureIgnoredChars()
Expand All @@ -34,6 +35,7 @@ class ParserOptionsTest extends Specification {

defaultOperationOptions.getMaxTokens() == 15_000
defaultOperationOptions.getMaxWhitespaceTokens() == 200_000
defaultOperationOptions.getMaxNumericLiteralCharacters() == 100
defaultOperationOptions.isCaptureSourceLocation()
!defaultOperationOptions.isCaptureLineComments()
!defaultOperationOptions.isCaptureIgnoredChars()
Expand All @@ -43,6 +45,7 @@ class ParserOptionsTest extends Specification {
defaultSdlOptions.getMaxCharacters() == Integer.MAX_VALUE
defaultSdlOptions.getMaxTokens() == Integer.MAX_VALUE
defaultSdlOptions.getMaxWhitespaceTokens() == Integer.MAX_VALUE
defaultSdlOptions.getMaxNumericLiteralCharacters() == 100
defaultSdlOptions.isCaptureSourceLocation()
defaultSdlOptions.isCaptureLineComments()
!defaultSdlOptions.isCaptureIgnoredChars()
Expand All @@ -61,6 +64,7 @@ class ParserOptionsTest extends Specification {
it.captureIgnoredChars(true)
.captureLineComments(true)
.maxCharacters(1_000_000)
.maxNumericLiteralCharacters(200)
.maxWhitespaceTokens(300_000)
})
def newDefaultSDlOptions = defaultSdlOptions.transform(
Expand All @@ -84,6 +88,7 @@ class ParserOptionsTest extends Specification {
currentDefaultOptions.getMaxCharacters() == ONE_MB
currentDefaultOptions.getMaxTokens() == 15_000
currentDefaultOptions.getMaxWhitespaceTokens() == 200_000
currentDefaultOptions.getMaxNumericLiteralCharacters() == 100
currentDefaultOptions.isCaptureSourceLocation()
currentDefaultOptions.isCaptureLineComments()
currentDefaultOptions.isCaptureIgnoredChars()
Expand All @@ -93,6 +98,7 @@ class ParserOptionsTest extends Specification {
currentDefaultOperationOptions.getMaxCharacters() == 1_000_000
currentDefaultOperationOptions.getMaxTokens() == 15_000
currentDefaultOperationOptions.getMaxWhitespaceTokens() == 300_000
currentDefaultOperationOptions.getMaxNumericLiteralCharacters() == 200
currentDefaultOperationOptions.isCaptureSourceLocation()
currentDefaultOperationOptions.isCaptureLineComments()
currentDefaultOperationOptions.isCaptureIgnoredChars()
Expand All @@ -102,6 +108,7 @@ class ParserOptionsTest extends Specification {
currentDefaultSdlOptions.getMaxCharacters() == Integer.MAX_VALUE
currentDefaultSdlOptions.getMaxTokens() == Integer.MAX_VALUE
currentDefaultSdlOptions.getMaxWhitespaceTokens() == 300_000
currentDefaultSdlOptions.getMaxNumericLiteralCharacters() == 100
currentDefaultSdlOptions.isCaptureSourceLocation()
currentDefaultSdlOptions.isCaptureLineComments()
currentDefaultSdlOptions.isCaptureIgnoredChars()
Expand Down
Loading
Loading