Skip to content

Commit

Permalink
parser: limit maximum number of tokens (#3702)
Browse files Browse the repository at this point in the history
Co-authored-by: Yaacov Rydzinski  <yaacovCR@gmail.com>
  • Loading branch information
IvanGoncharov and yaacovCR committed Aug 16, 2022
1 parent 6c6508b commit f0a0a4d
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 8 deletions.
13 changes: 13 additions & 0 deletions src/language/__tests__/parser-test.ts
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
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

0 comments on commit f0a0a4d

Please sign in to comment.