Skip to content

Commit f0a0a4d

Browse files
parser: limit maximum number of tokens (#3702)
Co-authored-by: Yaacov Rydzinski <[email protected]>
1 parent 6c6508b commit f0a0a4d

File tree

2 files changed

+48
-8
lines changed

2 files changed

+48
-8
lines changed

src/language/__tests__/parser-test.ts

+13
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

+35-8
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)