Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

17.x port - Stop DOS attacks by making the lexer stop early on evil input #2902

Merged
merged 3 commits into from Jul 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/pull_request.yml
Expand Up @@ -7,6 +7,7 @@ on:
pull_request:
branches:
- master
- 17.x
jobs:
buildAndTest:
runs-on: ubuntu-latest
Expand Down
10 changes: 9 additions & 1 deletion src/main/java/graphql/ParseAndValidate.java
Expand Up @@ -3,12 +3,15 @@
import graphql.language.Document;
import graphql.parser.InvalidSyntaxException;
import graphql.parser.Parser;
import graphql.parser.ParserOptions;
import graphql.schema.GraphQLSchema;
import graphql.validation.ValidationError;
import graphql.validation.Validator;

import java.util.List;

import static java.util.Optional.ofNullable;

/**
* This class allows you to parse and validate a graphql query without executing it. It will tell you
* if its syntactically valid and also semantically valid according to the graphql specification
Expand Down Expand Up @@ -42,8 +45,13 @@ public static ParseAndValidateResult parseAndValidate(GraphQLSchema graphQLSchem
*/
public static ParseAndValidateResult parse(ExecutionInput executionInput) {
try {
//
// we allow the caller to specify new parser options by context
ParserOptions parserOptions = executionInput.getGraphQLContext().get(ParserOptions.class);
// we use the query parser options by default if they are not specified
parserOptions = ofNullable(parserOptions).orElse(ParserOptions.getDefaultOperationParserOptions());
Parser parser = new Parser();
Document document = parser.parseDocument(executionInput.getQuery());
Document document = parser.parseDocument(executionInput.getQuery(),parserOptions);
return ParseAndValidateResult.newResult().document(document).variables(executionInput.getVariables()).build();
} catch (InvalidSyntaxException e) {
return ParseAndValidateResult.newResult().syntaxException(e).variables(executionInput.getVariables()).build();
Expand Down
12 changes: 7 additions & 5 deletions src/main/java/graphql/parser/GraphqlAntlrToLanguage.java
Expand Up @@ -76,14 +76,16 @@
import static graphql.Assert.assertShouldNeverHappen;
import static graphql.collect.ImmutableKit.emptyList;
import static graphql.collect.ImmutableKit.map;
import static graphql.parser.Parser.CHANNEL_COMMENTS;
import static graphql.parser.Parser.CHANNEL_WHITESPACE;
import static graphql.parser.StringValueParsing.parseSingleQuotedString;
import static graphql.parser.StringValueParsing.parseTripleQuotedString;
import static java.util.Optional.ofNullable;

@Internal
public class GraphqlAntlrToLanguage {

private static final int CHANNEL_COMMENTS = 2;
private static final int CHANNEL_IGNORED_CHARS = 3;
private static final List<Comment> NO_COMMENTS = ImmutableKit.emptyList();
private final CommonTokenStream tokens;
private final MultiSourceReader multiSourceReader;
private final ParserOptions parserOptions;
Expand All @@ -96,7 +98,7 @@ public GraphqlAntlrToLanguage(CommonTokenStream tokens, MultiSourceReader multiS
public GraphqlAntlrToLanguage(CommonTokenStream tokens, MultiSourceReader multiSourceReader, ParserOptions parserOptions) {
this.tokens = tokens;
this.multiSourceReader = multiSourceReader;
this.parserOptions = parserOptions == null ? ParserOptions.getDefaultParserOptions() : parserOptions;
this.parserOptions = ofNullable(parserOptions).orElse(ParserOptions.getDefaultParserOptions());
}

public ParserOptions getParserOptions() {
Expand Down Expand Up @@ -790,12 +792,12 @@ private void addIgnoredChars(ParserRuleContext ctx, NodeBuilder nodeBuilder) {
}
Token start = ctx.getStart();
int tokenStartIndex = start.getTokenIndex();
List<Token> leftChannel = tokens.getHiddenTokensToLeft(tokenStartIndex, CHANNEL_IGNORED_CHARS);
List<Token> leftChannel = tokens.getHiddenTokensToLeft(tokenStartIndex, CHANNEL_WHITESPACE);
List<IgnoredChar> ignoredCharsLeft = mapTokenToIgnoredChar(leftChannel);

Token stop = ctx.getStop();
int tokenStopIndex = stop.getTokenIndex();
List<Token> rightChannel = tokens.getHiddenTokensToRight(tokenStopIndex, CHANNEL_IGNORED_CHARS);
List<Token> rightChannel = tokens.getHiddenTokensToRight(tokenStopIndex, CHANNEL_WHITESPACE);
List<IgnoredChar> ignoredCharsRight = mapTokenToIgnoredChar(rightChannel);

nodeBuilder.ignoredChars(new IgnoredChars(ignoredCharsLeft, ignoredCharsRight));
Expand Down
45 changes: 35 additions & 10 deletions src/main/java/graphql/parser/Parser.java
@@ -1,5 +1,6 @@
package graphql.parser;

import graphql.Internal;
import graphql.PublicApi;
import graphql.language.Document;
import graphql.language.Node;
Expand All @@ -24,6 +25,8 @@
import java.io.Reader;
import java.io.UncheckedIOException;
import java.util.List;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;

/**
Expand All @@ -45,6 +48,11 @@
@PublicApi
public class Parser {

@Internal
public static final int CHANNEL_COMMENTS = 2;
@Internal
public static final int CHANNEL_WHITESPACE = 3;

/**
* Parses a string input into a graphql AST {@link Document}
*
Expand Down Expand Up @@ -195,7 +203,16 @@ public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int
}
});

CommonTokenStream tokens = new CommonTokenStream(lexer);
// default in the parser options if they are not set
parserOptions = Optional.ofNullable(parserOptions).orElse(ParserOptions.getDefaultParserOptions());

// this lexer wrapper allows us to stop lexing when too many tokens are in place. This prevents DOS attacks.
int maxTokens = parserOptions.getMaxTokens();
int maxWhitespaceTokens = parserOptions.getMaxWhitespaceTokens();
BiConsumer<Integer, Token> onTooManyTokens = (maxTokenCount, token) -> throwCancelParseIfTooManyTokens(token, maxTokenCount, multiSourceReader);
SafeTokenSource safeTokenSource = new SafeTokenSource(lexer, maxTokens, maxWhitespaceTokens, onTooManyTokens);

CommonTokenStream tokens = new CommonTokenStream(safeTokenSource);

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

@Override
public void visitTerminal(TerminalNode node) {
Token token = node.getSymbol();
count++;
if (count > maxTokens) {
String msg = String.format("More than %d parse tokens have been presented. To prevent Denial Of Service attacks, parsing has been cancelled.", maxTokens);
SourceLocation sourceLocation = null;
String offendingToken = null;
if (node.getSymbol() != null) {
offendingToken = node.getText();
sourceLocation = AntlrHelper.createSourceLocation(multiSourceReader, node.getSymbol().getLine(), node.getSymbol().getCharPositionInLine());
}

throw new ParseCancelledException(msg, sourceLocation, offendingToken);
throwCancelParseIfTooManyTokens(token, maxTokens, multiSourceReader);
}
}
};
parser.addParseListener(listener);
}

private void throwCancelParseIfTooManyTokens(Token token, int maxTokens, MultiSourceReader multiSourceReader) throws ParseCancelledException {
String tokenType = "grammar";
SourceLocation sourceLocation = null;
String offendingToken = null;
if (token != null) {
int channel = token.getChannel();
tokenType = channel == CHANNEL_WHITESPACE ? "whitespace" : (channel == CHANNEL_COMMENTS ? "comments" : "grammar");

offendingToken = token.getText();
sourceLocation = AntlrHelper.createSourceLocation(multiSourceReader, token.getLine(), token.getCharPositionInLine());
}
String msg = String.format("More than %d %s tokens have been presented. To prevent Denial Of Service attacks, parsing has been cancelled.", maxTokens, tokenType);
throw new ParseCancelledException(msg, sourceLocation, offendingToken);
}

/**
* Allows you to override the ANTLR to AST code.
*
Expand Down