Skip to content

Commit c267ae2

Browse files
committed
Wrap JSONPath compile time recursion errors
1 parent 8e7d138 commit c267ae2

9 files changed

Lines changed: 95 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Python JSONPath Change Log
22

3+
# Version 2.2.1 (unreleased)
4+
5+
**Fixes**
6+
7+
- JSONPath parser recursion limit failures now raise `JSONPathRecursionError`, preserving the original `RecursionError` as the cause. `JSONPathRecursionError` now also subclasses `RecursionError`, allowing existing except `RecursionError` handlers to continue working.
8+
39
## Version 2.2.0
410

511
**Fixes**

docs/index.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ Python JSONPath is a non-evaluating, read-only implementation of JSONPath, suita
66

77
We also include implementations of [JSON Pointer](pointers.md) ([RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901)) and [JSON Patch](api.md#jsonpath.JSONPatch) ([RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902)), plus methods for converting a [JSONPathMatch](api.md#jsonpath.JSONPathMatch) to a `JSONPointer`.
88

9+
!!! warning
10+
11+
If you are accepting JSONPath queries from untrusted users, make sure you handle `JSONPathRecursionError` (a subclass of `JSONPathError` and Python's `RecursionError`). `JSONPathRecursionError` can be thrown at compile time if a query has been crafted to cause our recursive parser to reach Python's recursion limit. Or at path resolution time if `max_recursion_depth` is reached with the descendent segment.
12+
913
## Install
1014

1115
Install Python JSONPath using [pip](https://pip.pypa.io/en/stable/getting-started/):

jsonpath/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# SPDX-FileCopyrightText: 2023-present James Prior <jamesgr.prior@gmail.com>
22
#
33
# SPDX-License-Identifier: MIT
4-
__version__ = "2.2.0"
4+
__version__ = "2.2.1"

jsonpath/__init__.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ def compile(path: str, *, strict: bool = False) -> Union[JSONPath, CompoundJSONP
111111
JSONPathSyntaxError: If _path_ is invalid.
112112
JSONPathTypeError: If filter functions are given arguments of an
113113
unacceptable type.
114+
JSONPathRecursionError: If _path_ has been crafted in such a way as to
115+
cause our recursive parser to reach Python's recursion limit.
116+
`JSONPathRecursionError` inherits from both `JSONPathError` and
117+
`RecursionError`.
114118
"""
115119
return _STRICT_ENV.compile(path) if strict else DEFAULT_ENV.compile(path)
116120

@@ -144,6 +148,10 @@ def findall(
144148
JSONPathSyntaxError: If the path is invalid.
145149
JSONPathTypeError: If a filter expression attempts to use types in
146150
an incompatible way.
151+
JSONPathRecursionError: At path resolution time if `max_recursion_depth`
152+
is reached with the descendent segment. Or at compile time if _path_
153+
has been crafted to cause our recursive parser to reach Python's
154+
recursion limit.
147155
"""
148156
return (
149157
_STRICT_ENV.findall(path, data, filter_context=filter_context)
@@ -181,6 +189,10 @@ async def findall_async(
181189
JSONPathSyntaxError: If the path is invalid.
182190
JSONPathTypeError: If a filter expression attempts to use types in
183191
an incompatible way.
192+
JSONPathRecursionError: At path resolution time if `max_recursion_depth`
193+
is reached with the descendent segment. Or at compile time if _path_
194+
has been crafted to cause our recursive parser to reach Python's
195+
recursion limit.
184196
"""
185197
return (
186198
await _STRICT_ENV.findall_async(path, data, filter_context=filter_context)
@@ -217,6 +229,10 @@ def finditer(
217229
JSONPathSyntaxError: If the path is invalid.
218230
JSONPathTypeError: If a filter expression attempts to use types in
219231
an incompatible way.
232+
JSONPathRecursionError: At path resolution time if `max_recursion_depth`
233+
is reached with the descendent segment. Or at compile time if _path_
234+
has been crafted to cause our recursive parser to reach Python's
235+
recursion limit.
220236
"""
221237
return (
222238
_STRICT_ENV.finditer(path, data, filter_context=filter_context)
@@ -254,6 +270,10 @@ async def finditer_async(
254270
JSONPathSyntaxError: If the path is invalid.
255271
JSONPathTypeError: If a filter expression attempts to use types in
256272
an incompatible way.
273+
JSONPathRecursionError: At path resolution time if `max_recursion_depth`
274+
is reached with the descendent segment. Or at compile time if _path_
275+
has been crafted to cause our recursive parser to reach Python's
276+
recursion limit.
257277
"""
258278
return (
259279
await _STRICT_ENV.finditer_async(path, data, filter_context=filter_context)
@@ -290,6 +310,10 @@ def match(
290310
JSONPathSyntaxError: If the path is invalid.
291311
JSONPathTypeError: If a filter expression attempts to use types in
292312
an incompatible way.
313+
JSONPathRecursionError: At path resolution time if `max_recursion_depth`
314+
is reached with the descendent segment. Or at compile time if _path_
315+
has been crafted to cause our recursive parser to reach Python's
316+
recursion limit.
293317
"""
294318
return (
295319
_STRICT_ENV.match(path, data, filter_context=filter_context)
@@ -357,6 +381,10 @@ def query(
357381
JSONPathSyntaxError: If the path is invalid.
358382
JSONPathTypeError: If a filter expression attempts to use types in
359383
an incompatible way.
384+
JSONPathRecursionError: At path resolution time if `max_recursion_depth`
385+
is reached with the descendent segment. Or at compile time if _path_
386+
has been crafted to cause our recursive parser to reach Python's
387+
recursion limit.
360388
"""
361389
return (
362390
_STRICT_ENV.query(path, data, filter_context=filter_context)

jsonpath/env.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,10 @@ def compile(self, path: str) -> Union[JSONPath, CompoundJSONPath]: # noqa: A003
217217
JSONPathSyntaxError: If _path_ is invalid.
218218
JSONPathTypeError: If filter functions are given arguments of an
219219
unacceptable type.
220+
JSONPathRecursionError: If _path_ has been crafted in such a way as
221+
to cause our recursive parser to reach Python's recursion limit.
222+
JSONPathRecursionError inherits from both `JSONPathError` and
223+
`RecursionError`.
220224
"""
221225
tokens = self.lexer.tokenize(path)
222226
stream = TokenStream(tokens)
@@ -298,6 +302,10 @@ def findall(
298302
JSONPathSyntaxError: If the path is invalid.
299303
JSONPathTypeError: If a filter expression attempts to use types in
300304
an incompatible way.
305+
JSONPathRecursionError: At path resolution time if `max_recursion_depth`
306+
is reached with the descendent segment. Or at compile time if
307+
_path_ has been crafted to cause our recursive parser to reach
308+
Python's recursion limit.
301309
"""
302310
return self.compile(path).findall(data, filter_context=filter_context)
303311

@@ -327,6 +335,10 @@ def finditer(
327335
JSONPathSyntaxError: If the path is invalid.
328336
JSONPathTypeError: If a filter expression attempts to use types in
329337
an incompatible way.
338+
JSONPathRecursionError: At path resolution time if `max_recursion_depth`
339+
is reached with the descendent segment. Or at compile time if
340+
_path_ has been crafted to cause our recursive parser to reach
341+
Python's recursion limit.
330342
"""
331343
return self.compile(path).finditer(data, filter_context=filter_context)
332344

@@ -356,6 +368,10 @@ def match(
356368
JSONPathSyntaxError: If the path is invalid.
357369
JSONPathTypeError: If a filter expression attempts to use types in
358370
an incompatible way.
371+
JSONPathRecursionError: At path resolution time if `max_recursion_depth`
372+
is reached with the descendent segment. Or at compile time if
373+
_path_ has been crafted to cause our recursive parser to reach
374+
Python's recursion limit.
359375
"""
360376
return self.compile(path).match(data, filter_context=filter_context)
361377

@@ -416,6 +432,10 @@ def query(
416432
JSONPathSyntaxError: If the path is invalid.
417433
JSONPathTypeError: If a filter expression attempts to use types in
418434
an incompatible way.
435+
JSONPathRecursionError: At path resolution time if `max_recursion_depth`
436+
is reached with the descendent segment. Or at compile time if
437+
_path_ has been crafted to cause our recursive parser to reach
438+
Python's recursion limit.
419439
"""
420440
return Query(self.finditer(path, data, filter_context=filter_context), self)
421441

jsonpath/exceptions.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,15 +135,19 @@ def __init__(self, *args: object, token: Token) -> None:
135135
self.token = token
136136

137137

138-
class JSONPathRecursionError(JSONPathError):
138+
class JSONPathRecursionError(JSONPathError, RecursionError):
139139
"""An exception raised when the maximum recursion depth is reached.
140140
141+
This could be raised at parse time if a JSONPath query is constructed in
142+
such a way to hit Python's recursion limit. Or at query resolution time
143+
if `max_recursion_depth` is reached by the descendant segment (`..`).
144+
141145
Arguments:
142146
args: Arguments passed to `Exception`.
143147
token: The token that caused the error.
144148
"""
145149

146-
def __init__(self, *args: object, token: Token) -> None:
150+
def __init__(self, *args: object, token: Optional[Token]) -> None:
147151
super().__init__(*args)
148152
self.token = token
149153

jsonpath/parse.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from jsonpath.function_extensions.filter_function import ExpressionType
1717
from jsonpath.function_extensions.filter_function import FilterFunction
1818

19+
from .exceptions import JSONPathRecursionError
1920
from .exceptions import JSONPathSyntaxError
2021
from .exceptions import JSONPathTypeError
2122
from .filter import CURRENT_KEY
@@ -324,7 +325,10 @@ def parse(self, stream: TokenStream) -> Iterator[JSONPathSegment]:
324325
# Raises a syntax error because the current token is not TOKEN_ROOT.
325326
stream.expect(TOKEN_ROOT)
326327

327-
yield from self.parse_query(stream)
328+
try:
329+
yield from self.parse_query(stream)
330+
except RecursionError as err:
331+
raise JSONPathRecursionError(str(err), token=None) from err
328332

329333
if stream.current().kind not in (TOKEN_EOF, TOKEN_INTERSECTION, TOKEN_UNION):
330334
raise JSONPathSyntaxError(

jsonpath/path.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ def findall(
8787
JSONPathSyntaxError: If the path is invalid.
8888
JSONPathTypeError: If a filter expression attempts to use types in
8989
an incompatible way.
90+
JSONPathRecursionError: If `max_recursion_depth` is reached with
91+
the descendent segment.
9092
"""
9193
return [
9294
match.obj for match in self.finditer(data, filter_context=filter_context)
@@ -113,6 +115,8 @@ def finditer(
113115
JSONPathSyntaxError: If the path is invalid.
114116
JSONPathTypeError: If a filter expression attempts to use types in
115117
an incompatible way.
118+
JSONPathRecursionError: If `max_recursion_depth` is reached with
119+
the descendent segment.
116120
"""
117121
_data = load_data(data)
118122
path = self.env.pseudo_root_token if self.pseudo_root else self.env.root_token
@@ -189,6 +193,8 @@ def match(
189193
JSONPathSyntaxError: If the path is invalid.
190194
JSONPathTypeError: If a filter expression attempts to use types in
191195
an incompatible way.
196+
JSONPathRecursionError: If `max_recursion_depth` is reached with
197+
the descendent segment.
192198
"""
193199
try:
194200
return next(iter(self.finditer(data, filter_context=filter_context)))
@@ -213,6 +219,8 @@ def query(
213219
JSONPathSyntaxError: If the path is invalid.
214220
JSONPathTypeError: If a filter expression attempts to use types in
215221
an incompatible way.
222+
JSONPathRecursionError: If `max_recursion_depth` is reached with
223+
the descendent segment.
216224
"""
217225
return Query(self.finditer(data, filter_context=filter_context), self.env)
218226

@@ -291,6 +299,9 @@ def findall(
291299
JSONPathSyntaxError: If the path is invalid.
292300
JSONPathTypeError: If a filter expression attempts to use types in
293301
an incompatible way.
302+
JSONPathRecursionError: If `max_recursion_depth` is reached with
303+
the descendent segment.
304+
294305
"""
295306
objs = self.path.findall(data, filter_context=filter_context)
296307

@@ -325,6 +336,8 @@ def finditer(
325336
JSONPathSyntaxError: If the path is invalid.
326337
JSONPathTypeError: If a filter expression attempts to use types in
327338
an incompatible way.
339+
JSONPathRecursionError: If `max_recursion_depth` is reached with
340+
the descendent segment.
328341
"""
329342
matches = self.path.finditer(data, filter_context=filter_context)
330343

@@ -360,6 +373,8 @@ def match(
360373
JSONPathSyntaxError: If the path is invalid.
361374
JSONPathTypeError: If a filter expression attempts to use types in
362375
an incompatible way.
376+
JSONPathRecursionError: If `max_recursion_depth` is reached with
377+
the descendent segment.
363378
"""
364379
try:
365380
return next(iter(self.finditer(data, filter_context=filter_context)))
@@ -417,6 +432,8 @@ def query(
417432
JSONPathSyntaxError: If the path is invalid.
418433
JSONPathTypeError: If a filter expression attempts to use types in
419434
an incompatible way.
435+
JSONPathRecursionError: If `max_recursion_depth` is reached with
436+
the descendent segment.
420437
"""
421438
return Query(self.finditer(data, filter_context=filter_context), self.env)
422439

tests/test_errors.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,11 @@ class MockEnv(JSONPathEnvironment):
105105

106106
with pytest.raises(JSONPathRecursionError):
107107
env.findall(query, data)
108+
109+
110+
def test_compile_time_recursion_error(env: JSONPathEnvironment) -> None:
111+
with pytest.raises(JSONPathRecursionError):
112+
env.compile("$[?" + "!" * 493 + "@.a]")
113+
114+
with pytest.raises(RecursionError):
115+
env.compile("$[?" + "!" * 493 + "@.a]")

0 commit comments

Comments
 (0)