Skip to content

Commit 80ef435

Browse files
committed
fix: count comment tokens toward the parser max_tokens limit
Comment tokens are read, allocated as Token objects, and chained into the token list, but advance_lexer only counted the non-comment tokens it returned. A comment-padded document could bypass max_tokens while still allocating one token per comment, exhausting memory. Count the comments skipped between the previous and current token (including the advance onto EOF, covering trailing comments) so every allocated token counts toward the limit.
1 parent 29c2753 commit 80ef435

2 files changed

Lines changed: 55 additions & 11 deletions

File tree

‎src/graphql/language/parser.py‎

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1355,18 +1355,33 @@ def delimited_many(
13551355
return nodes
13561356

13571357
def advance_lexer(self) -> None:
1358-
token = self._lexer.advance()
1359-
1360-
if token.kind != TokenKind.EOF:
1358+
"""Advance the lexer.
1359+
1360+
Counts every token that is consumed toward ``max_tokens``, including the
1361+
comment tokens that the lexer skips over between the previously focused token
1362+
and the new one. Comments are read and allocated as ``Token`` objects (and
1363+
chained into the token list) but are not returned by the lexer, so counting
1364+
only the returned tokens would let a comment-padded document bypass
1365+
``max_tokens`` while still allocating one token per comment.
1366+
"""
1367+
lexer = self._lexer
1368+
token = lexer.advance()
1369+
# Count the comment tokens that were skipped since the previously focused
1370+
# token. They are the nodes strictly between ``last_token`` and ``token`` in
1371+
# the token linked-list (the lexer only ever skips comments).
1372+
skipped = lexer.last_token.next
1373+
while skipped is not None and skipped is not token:
13611374
self._token_counter += 1
1362-
max_tokens = self._max_tokens
1363-
if max_tokens is not None and self._token_counter > max_tokens:
1364-
raise GraphQLSyntaxError(
1365-
self._lexer.source,
1366-
token.start,
1367-
f"Document contains more than {max_tokens} tokens."
1368-
" Parsing aborted.",
1369-
)
1375+
skipped = skipped.next
1376+
if token.kind is not TokenKind.EOF:
1377+
self._token_counter += 1
1378+
max_tokens = self._max_tokens
1379+
if max_tokens is not None and self._token_counter > max_tokens:
1380+
raise GraphQLSyntaxError(
1381+
lexer.source,
1382+
token.start,
1383+
f"Document contains more than {max_tokens} tokens. Parsing aborted.",
1384+
)
13701385

13711386

13721387
def get_token_desc(token: Token) -> str:

‎tests/language/test_parser.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,35 @@ def limits_maximum_number_of_tokens():
116116
):
117117
assert parse('{ foo(bar: "baz") }', max_tokens=7)
118118

119+
def counts_comment_tokens_towards_the_maximum():
120+
# Comment tokens are read and allocated by the lexer but skipped over; they
121+
# must still count towards max_tokens, otherwise a comment-padded document
122+
# bypasses the limit (memory-exhaustion DoS) while allocating one token per
123+
# comment. token_count reflects the comments too.
124+
assert parse("{ # comment\n foo }").token_count == 4
125+
assert parse("# leading\n{ foo }\n# trailing").token_count == 5
126+
127+
# An interspersed comment pushes the document over the limit.
128+
parse("{ # c\n foo }", max_tokens=4)
129+
with raises(
130+
GraphQLSyntaxError,
131+
match="Syntax Error:"
132+
r" Document contains more than 3 tokens\. Parsing aborted\.",
133+
):
134+
parse("{ # c\n foo }", max_tokens=3)
135+
136+
# Neither leading nor trailing comment padding can bypass the limit.
137+
with raises(
138+
GraphQLSyntaxError,
139+
match=r"Document contains more than 5 tokens\. Parsing aborted\.",
140+
):
141+
parse("# c\n" * 1000 + "{ a }", max_tokens=5)
142+
with raises(
143+
GraphQLSyntaxError,
144+
match=r"Document contains more than 5 tokens\. Parsing aborted\.",
145+
):
146+
parse("{ a }" + "\n# c" * 1000, max_tokens=5)
147+
119148
def parses_variable_inline_values():
120149
parse("{ field(complex: { a: { b: [ $var ] } }) }")
121150

0 commit comments

Comments
 (0)