Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 37 additions & 17 deletions jmespath/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,21 @@ class Parser(object):
# _CACHE dict.
_CACHE = {}
_MAX_SIZE = 512
# _expression() recurses once per nesting level of the parsed
# expression with no other bound, so a deeply nested expression
# string can exhaust the interpreter's call stack. This caps the
# nesting depth well above anything a hand-written or generated
# expression would realistically use, and turns what would
# otherwise be a bare, uncatchable-by-callers-following-our-own-
# docs RecursionError into a regular ParseError.
_MAX_EXPRESSION_DEPTH = 100

def __init__(self, lookahead=2):
self.tokenizer = None
self._tokens = [None] * lookahead
self._buffer_size = lookahead
self._index = 0
self._depth = 0

def parse(self, expression):
try:
Expand Down Expand Up @@ -119,6 +128,7 @@ def _parse(self, expression):
self.tokenizer = lexer.Lexer().tokenize(expression)
self._tokens = list(self.tokenizer)
self._index = 0
self._depth = 0
parsed = self._expression(binding_power=0)
if not self._current_token() == 'eof':
t = self._lookahead_token(0)
Expand All @@ -127,23 +137,33 @@ def _parse(self, expression):
return ParsedResult(expression, parsed)

def _expression(self, binding_power=0):
left_token = self._lookahead_token(0)
self._advance()
nud_function = getattr(
self, '_token_nud_%s' % left_token['type'],
self._error_nud_token)
left = nud_function(left_token)
current_token = self._current_token()
while binding_power < self.BINDING_POWER[current_token]:
led = getattr(self, '_token_led_%s' % current_token, None)
if led is None:
error_token = self._lookahead_token(0)
self._error_led_token(error_token)
else:
self._advance()
left = led(left)
current_token = self._current_token()
return left
self._depth += 1
if self._depth > self._MAX_EXPRESSION_DEPTH:
t = self._lookahead_token(0)
raise exceptions.ParseError(
t['start'], t['value'], t['type'],
"Expression nested too deeply (max %d levels)"
% self._MAX_EXPRESSION_DEPTH)
try:
left_token = self._lookahead_token(0)
self._advance()
nud_function = getattr(
self, '_token_nud_%s' % left_token['type'],
self._error_nud_token)
left = nud_function(left_token)
current_token = self._current_token()
while binding_power < self.BINDING_POWER[current_token]:
led = getattr(self, '_token_led_%s' % current_token, None)
if led is None:
error_token = self._lookahead_token(0)
self._error_led_token(error_token)
else:
self._advance()
left = led(left)
current_token = self._current_token()
return left
finally:
self._depth -= 1

def _token_nud_literal(self, token):
return ast.literal(token['value'])
Expand Down
42 changes: 42 additions & 0 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,5 +390,47 @@ def test_dot_file_subexpr(self):
'field3 [label="field(bar)"]\n}')


class TestParserDepthLimit(unittest.TestCase):
# _expression() recurses once per nesting level with no bound, so a
# deeply nested expression could exhaust the interpreter's call stack
# and raise a bare RecursionError - not jmespath.exceptions.ParseError
# or any other member of the library's own exception hierarchy - which
# callers following the documented error-handling pattern (catching
# only JMESPathError around compile()/search()) would not catch.

def test_deeply_nested_expression_raises_parse_error(self):
p = parser.Parser()
expression = '[' * 50000 + ']' * 50000
with self.assertRaises(exceptions.ParseError):
p.parse(expression)

def test_deeply_nested_expression_does_not_raise_recursion_error(self):
p = parser.Parser()
expression = '[' * 50000 + ']' * 50000
try:
p.parse(expression)
except RecursionError:
self.fail("RecursionError leaked past the depth limit check")
except exceptions.ParseError:
pass

def test_reasonably_nested_expression_still_parses(self):
p = parser.Parser()
depth = parser.Parser._MAX_EXPRESSION_DEPTH - 10
expression = '[' * depth + ']' * depth
# Should not raise.
p.parse(expression)

def test_depth_resets_between_parses_on_same_parser_instance(self):
p = parser.Parser()
too_deep = '[' * 50000 + ']' * 50000
with self.assertRaises(exceptions.ParseError):
p.parse(too_deep)
# A later, unrelated, shallow expression on the same Parser
# instance must not be affected by the earlier failed parse.
result = p.parse('foo.bar')
self.assertIsNotNone(result)


if __name__ == '__main__':
unittest.main()