Skip to content

Commit 55190aa

Browse files
parser: limit maximum number of tokens
Backport of graphql#3684 Motivation: 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. On my mashine it happens on queries with 2k tokens. For example: ``` { a a <repeat 2k times> a } ``` It takes 741ms on my machine. But if we create document of the same size but smaller number of tokens it would be a lot faster. Example: ``` { a(arg: "a <repeat 2k times> a" } ``` Now it takes only 17ms to process, which is 43 time faster. That mean if we limit document size we should make this limit small since it take only two bytes to create a token, e.g. ` a`. But that will hart legit documents that have long tokens in them (comments, describtions, strings, long names, etc.). That's why this PR adds a mechanism to limit number of token in parsed document. Also exact same mechanism implemented in graphql-java, see: graphql-java/graphql-java#2549 I also tried alternative approach of counting nodes and it gives slightly better approximation of how many resources would be consumed. However comparing to the tokens, AST nodes is implementation detail of graphql-js so it's imposible to replicate in other implementation (e.g. to count this number on a client). * Apply suggestions from code review Co-authored-by: Yaacov Rydzinski <yaacovCR@gmail.com> Co-authored-by: Yaacov Rydzinski <yaacovCR@gmail.com>
1 parent 86feb78 commit 55190aa

File tree

2 files changed

+48
-8
lines changed

2 files changed

+48
-8
lines changed

src/language/__tests__/parser-test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,19 @@ describe('Parser', () => {
8484
`);
8585
});
8686

87+
it('limit maximum number of tokens', () => {
88+
expect(() => parse('{ foo }', { maxTokens: 3 })).to.not.throw();
89+
expect(() => parse('{ foo }', { maxTokens: 2 })).to.throw(
90+
'Syntax Error: Document contains more that 2 tokens. Parsing aborted.',
91+
);
92+
93+
expect(() => parse('{ foo(bar: "baz") }', { maxTokens: 8 })).to.not.throw();
94+
95+
expect(() => parse('{ foo(bar: "baz") }', { maxTokens: 7 })).to.throw(
96+
'Syntax Error: Document contains more that 7 tokens. Parsing aborted.',
97+
);
98+
});
99+
87100
it('parses variable inline values', () => {
88101
expect(() =>
89102
parse('{ field(complex: { a: { b: [ $var ] } }) }'),

src/language/parser.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@ export interface ParseOptions {
7878
*/
7979
noLocation?: boolean;
8080

81+
/**
82+
* Parser CPU and memory usage is linear to the number of tokens in a document
83+
* however in extreme cases it becomes quadratic due to memory exhaustion.
84+
* Parsing happens before validation so even invalid queries can burn lots of
85+
* CPU time and memory.
86+
* To prevent this you can set a maximum number of tokens allowed within a document.
87+
*/
88+
maxTokens?: number | undefined;
89+
8190
/**
8291
* @deprecated will be removed in the v17.0.0
8392
*
@@ -179,12 +188,14 @@ export function parseType(
179188
export class Parser {
180189
protected _options: ParseOptions;
181190
protected _lexer: Lexer;
191+
protected _tokenCounter: number;
182192

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

186196
this._lexer = new Lexer(sourceObj);
187197
this._options = options;
198+
this._tokenCounter = 0;
188199
}
189200

190201
/**
@@ -569,13 +580,13 @@ export class Parser {
569580
case TokenKind.BRACE_L:
570581
return this.parseObject(isConst);
571582
case TokenKind.INT:
572-
this._lexer.advance();
583+
this.advanceLexer();
573584
return this.node<IntValueNode>(token, {
574585
kind: Kind.INT,
575586
value: token.value,
576587
});
577588
case TokenKind.FLOAT:
578-
this._lexer.advance();
589+
this.advanceLexer();
579590
return this.node<FloatValueNode>(token, {
580591
kind: Kind.FLOAT,
581592
value: token.value,
@@ -584,7 +595,7 @@ export class Parser {
584595
case TokenKind.BLOCK_STRING:
585596
return this.parseStringLiteral();
586597
case TokenKind.NAME:
587-
this._lexer.advance();
598+
this.advanceLexer();
588599
switch (token.value) {
589600
case 'true':
590601
return this.node<BooleanValueNode>(token, {
@@ -630,7 +641,7 @@ export class Parser {
630641

631642
parseStringLiteral(): StringValueNode {
632643
const token = this._lexer.token;
633-
this._lexer.advance();
644+
this.advanceLexer();
634645
return this.node<StringValueNode>(token, {
635646
kind: Kind.STRING,
636647
value: token.value,
@@ -1411,7 +1422,7 @@ export class Parser {
14111422
expectToken(kind: TokenKind): Token {
14121423
const token = this._lexer.token;
14131424
if (token.kind === kind) {
1414-
this._lexer.advance();
1425+
this.advanceLexer();
14151426
return token;
14161427
}
14171428

@@ -1429,7 +1440,7 @@ export class Parser {
14291440
expectOptionalToken(kind: TokenKind): boolean {
14301441
const token = this._lexer.token;
14311442
if (token.kind === kind) {
1432-
this._lexer.advance();
1443+
this.advanceLexer();
14331444
return true;
14341445
}
14351446
return false;
@@ -1442,7 +1453,7 @@ export class Parser {
14421453
expectKeyword(value: string): void {
14431454
const token = this._lexer.token;
14441455
if (token.kind === TokenKind.NAME && token.value === value) {
1445-
this._lexer.advance();
1456+
this.advanceLexer();
14461457
} else {
14471458
throw syntaxError(
14481459
this._lexer.source,
@@ -1459,7 +1470,7 @@ export class Parser {
14591470
expectOptionalKeyword(value: string): boolean {
14601471
const token = this._lexer.token;
14611472
if (token.kind === TokenKind.NAME && token.value === value) {
1462-
this._lexer.advance();
1473+
this.advanceLexer();
14631474
return true;
14641475
}
14651476
return false;
@@ -1548,6 +1559,22 @@ export class Parser {
15481559
} while (this.expectOptionalToken(delimiterKind));
15491560
return nodes;
15501561
}
1562+
1563+
advanceLexer(): void {
1564+
const { maxTokens } = this._options;
1565+
const token = this._lexer.advance();
1566+
1567+
if (maxTokens !== undefined && token.kind !== TokenKind.EOF) {
1568+
++this._tokenCounter;
1569+
if (this._tokenCounter > maxTokens) {
1570+
throw syntaxError(
1571+
this._lexer.source,
1572+
token.start,
1573+
`Document contains more that ${maxTokens} tokens. Parsing aborted.`,
1574+
);
1575+
}
1576+
}
1577+
}
15511578
}
15521579

15531580
/**

0 commit comments

Comments
 (0)