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

parser: limit maximum number of tokens #3702

Merged
merged 1 commit into from
Aug 16, 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
13 changes: 13 additions & 0 deletions src/language/__tests__/parser-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,19 @@ describe('Parser', () => {
`);
});

it('limit maximum number of tokens', () => {
expect(() => parse('{ foo }', { maxTokens: 3 })).to.not.throw();
expect(() => parse('{ foo }', { maxTokens: 2 })).to.throw(
'Syntax Error: Document contains more that 2 tokens. Parsing aborted.',
);

expect(() => parse('{ foo(bar: "baz") }', { maxTokens: 8 })).to.not.throw();

expect(() => parse('{ foo(bar: "baz") }', { maxTokens: 7 })).to.throw(
'Syntax Error: Document contains more that 7 tokens. Parsing aborted.',
);
});

it('parses variable inline values', () => {
expect(() =>
parse('{ field(complex: { a: { b: [ $var ] } }) }'),
Expand Down
43 changes: 35 additions & 8 deletions src/language/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ export interface ParseOptions {
*/
noLocation?: boolean;

/**
* Parser CPU and memory usage is linear to the number of tokens in a document
* however in extreme cases it becomes quadratic due to memory exhaustion.
* Parsing happens before validation so even invalid queries can burn lots of
* CPU time and memory.
* To prevent this you can set a maximum number of tokens allowed within a document.
*/
maxTokens?: number | undefined;

/**
* @deprecated will be removed in the v17.0.0
*
Expand Down Expand Up @@ -179,12 +188,14 @@ export function parseType(
export class Parser {
protected _options: ParseOptions;
protected _lexer: Lexer;
protected _tokenCounter: number;

constructor(source: string | Source, options: ParseOptions = {}) {
const sourceObj = isSource(source) ? source : new Source(source);

this._lexer = new Lexer(sourceObj);
this._options = options;
this._tokenCounter = 0;
}

/**
Expand Down Expand Up @@ -569,13 +580,13 @@ export class Parser {
case TokenKind.BRACE_L:
return this.parseObject(isConst);
case TokenKind.INT:
this._lexer.advance();
this.advanceLexer();
return this.node<IntValueNode>(token, {
kind: Kind.INT,
value: token.value,
});
case TokenKind.FLOAT:
this._lexer.advance();
this.advanceLexer();
return this.node<FloatValueNode>(token, {
kind: Kind.FLOAT,
value: token.value,
Expand All @@ -584,7 +595,7 @@ export class Parser {
case TokenKind.BLOCK_STRING:
return this.parseStringLiteral();
case TokenKind.NAME:
this._lexer.advance();
this.advanceLexer();
switch (token.value) {
case 'true':
return this.node<BooleanValueNode>(token, {
Expand Down Expand Up @@ -630,7 +641,7 @@ export class Parser {

parseStringLiteral(): StringValueNode {
const token = this._lexer.token;
this._lexer.advance();
this.advanceLexer();
return this.node<StringValueNode>(token, {
kind: Kind.STRING,
value: token.value,
Expand Down Expand Up @@ -1411,7 +1422,7 @@ export class Parser {
expectToken(kind: TokenKind): Token {
const token = this._lexer.token;
if (token.kind === kind) {
this._lexer.advance();
this.advanceLexer();
return token;
}

Expand All @@ -1429,7 +1440,7 @@ export class Parser {
expectOptionalToken(kind: TokenKind): boolean {
const token = this._lexer.token;
if (token.kind === kind) {
this._lexer.advance();
this.advanceLexer();
return true;
}
return false;
Expand All @@ -1442,7 +1453,7 @@ export class Parser {
expectKeyword(value: string): void {
const token = this._lexer.token;
if (token.kind === TokenKind.NAME && token.value === value) {
this._lexer.advance();
this.advanceLexer();
} else {
throw syntaxError(
this._lexer.source,
Expand All @@ -1459,7 +1470,7 @@ export class Parser {
expectOptionalKeyword(value: string): boolean {
const token = this._lexer.token;
if (token.kind === TokenKind.NAME && token.value === value) {
this._lexer.advance();
this.advanceLexer();
return true;
}
return false;
Expand Down Expand Up @@ -1548,6 +1559,22 @@ export class Parser {
} while (this.expectOptionalToken(delimiterKind));
return nodes;
}

advanceLexer(): void {
const { maxTokens } = this._options;
const token = this._lexer.advance();

if (maxTokens !== undefined && token.kind !== TokenKind.EOF) {
++this._tokenCounter;
if (this._tokenCounter > maxTokens) {
throw syntaxError(
this._lexer.source,
token.start,
`Document contains more that ${maxTokens} tokens. Parsing aborted.`,
);
}
}
}
}

/**
Expand Down