Skip to content
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Python JSONPath Change Log

# Version 2.2.1 (unreleased)

**Fixes**

- 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.

## Version 2.2.0

**Fixes**
Expand Down
4 changes: 4 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ Python JSONPath is a non-evaluating, read-only implementation of JSONPath, suita

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`.

!!! warning

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.

## Install

Install Python JSONPath using [pip](https://pip.pypa.io/en/stable/getting-started/):
Expand Down
2 changes: 1 addition & 1 deletion jsonpath/__about__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2023-present James Prior <jamesgr.prior@gmail.com>
#
# SPDX-License-Identifier: MIT
__version__ = "2.2.0"
__version__ = "2.2.1"
28 changes: 28 additions & 0 deletions jsonpath/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ def compile(path: str, *, strict: bool = False) -> Union[JSONPath, CompoundJSONP
JSONPathSyntaxError: If _path_ is invalid.
JSONPathTypeError: If filter functions are given arguments of an
unacceptable type.
JSONPathRecursionError: If _path_ has been crafted in such a way as to
cause our recursive parser to reach Python's recursion limit.
`JSONPathRecursionError` inherits from both `JSONPathError` and
`RecursionError`.
"""
return _STRICT_ENV.compile(path) if strict else DEFAULT_ENV.compile(path)

Expand Down Expand Up @@ -144,6 +148,10 @@ def findall(
JSONPathSyntaxError: If the path is invalid.
JSONPathTypeError: If a filter expression attempts to use types in
an incompatible way.
JSONPathRecursionError: At path resolution time if `max_recursion_depth`
is reached with the descendent segment. Or at compile time if _path_
has been crafted to cause our recursive parser to reach Python's
recursion limit.
"""
return (
_STRICT_ENV.findall(path, data, filter_context=filter_context)
Expand Down Expand Up @@ -181,6 +189,10 @@ async def findall_async(
JSONPathSyntaxError: If the path is invalid.
JSONPathTypeError: If a filter expression attempts to use types in
an incompatible way.
JSONPathRecursionError: At path resolution time if `max_recursion_depth`
is reached with the descendent segment. Or at compile time if _path_
has been crafted to cause our recursive parser to reach Python's
recursion limit.
"""
return (
await _STRICT_ENV.findall_async(path, data, filter_context=filter_context)
Expand Down Expand Up @@ -217,6 +229,10 @@ def finditer(
JSONPathSyntaxError: If the path is invalid.
JSONPathTypeError: If a filter expression attempts to use types in
an incompatible way.
JSONPathRecursionError: At path resolution time if `max_recursion_depth`
is reached with the descendent segment. Or at compile time if _path_
has been crafted to cause our recursive parser to reach Python's
recursion limit.
"""
return (
_STRICT_ENV.finditer(path, data, filter_context=filter_context)
Expand Down Expand Up @@ -254,6 +270,10 @@ async def finditer_async(
JSONPathSyntaxError: If the path is invalid.
JSONPathTypeError: If a filter expression attempts to use types in
an incompatible way.
JSONPathRecursionError: At path resolution time if `max_recursion_depth`
is reached with the descendent segment. Or at compile time if _path_
has been crafted to cause our recursive parser to reach Python's
recursion limit.
"""
return (
await _STRICT_ENV.finditer_async(path, data, filter_context=filter_context)
Expand Down Expand Up @@ -290,6 +310,10 @@ def match(
JSONPathSyntaxError: If the path is invalid.
JSONPathTypeError: If a filter expression attempts to use types in
an incompatible way.
JSONPathRecursionError: At path resolution time if `max_recursion_depth`
is reached with the descendent segment. Or at compile time if _path_
has been crafted to cause our recursive parser to reach Python's
recursion limit.
"""
return (
_STRICT_ENV.match(path, data, filter_context=filter_context)
Expand Down Expand Up @@ -357,6 +381,10 @@ def query(
JSONPathSyntaxError: If the path is invalid.
JSONPathTypeError: If a filter expression attempts to use types in
an incompatible way.
JSONPathRecursionError: At path resolution time if `max_recursion_depth`
is reached with the descendent segment. Or at compile time if _path_
has been crafted to cause our recursive parser to reach Python's
recursion limit.
"""
return (
_STRICT_ENV.query(path, data, filter_context=filter_context)
Expand Down
20 changes: 20 additions & 0 deletions jsonpath/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ def compile(self, path: str) -> Union[JSONPath, CompoundJSONPath]: # noqa: A003
JSONPathSyntaxError: If _path_ is invalid.
JSONPathTypeError: If filter functions are given arguments of an
unacceptable type.
JSONPathRecursionError: If _path_ has been crafted in such a way as
to cause our recursive parser to reach Python's recursion limit.
JSONPathRecursionError inherits from both `JSONPathError` and
`RecursionError`.
"""
tokens = self.lexer.tokenize(path)
stream = TokenStream(tokens)
Expand Down Expand Up @@ -298,6 +302,10 @@ def findall(
JSONPathSyntaxError: If the path is invalid.
JSONPathTypeError: If a filter expression attempts to use types in
an incompatible way.
JSONPathRecursionError: At path resolution time if `max_recursion_depth`
is reached with the descendent segment. Or at compile time if
_path_ has been crafted to cause our recursive parser to reach
Python's recursion limit.
"""
return self.compile(path).findall(data, filter_context=filter_context)

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

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

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

Expand Down
8 changes: 6 additions & 2 deletions jsonpath/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,15 +135,19 @@ def __init__(self, *args: object, token: Token) -> None:
self.token = token


class JSONPathRecursionError(JSONPathError):
class JSONPathRecursionError(JSONPathError, RecursionError):
"""An exception raised when the maximum recursion depth is reached.

This could be raised at parse time if a JSONPath query is constructed in
such a way to hit Python's recursion limit. Or at query resolution time
if `max_recursion_depth` is reached by the descendant segment (`..`).

Arguments:
args: Arguments passed to `Exception`.
token: The token that caused the error.
"""

def __init__(self, *args: object, token: Token) -> None:
def __init__(self, *args: object, token: Optional[Token]) -> None:
super().__init__(*args)
self.token = token

Expand Down
6 changes: 5 additions & 1 deletion jsonpath/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from jsonpath.function_extensions.filter_function import ExpressionType
from jsonpath.function_extensions.filter_function import FilterFunction

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

yield from self.parse_query(stream)
try:
yield from self.parse_query(stream)
except RecursionError as err:
raise JSONPathRecursionError(str(err), token=None) from err

if stream.current().kind not in (TOKEN_EOF, TOKEN_INTERSECTION, TOKEN_UNION):
raise JSONPathSyntaxError(
Expand Down
17 changes: 17 additions & 0 deletions jsonpath/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ def findall(
JSONPathSyntaxError: If the path is invalid.
JSONPathTypeError: If a filter expression attempts to use types in
an incompatible way.
JSONPathRecursionError: If `max_recursion_depth` is reached with
the descendent segment.
"""
return [
match.obj for match in self.finditer(data, filter_context=filter_context)
Expand All @@ -113,6 +115,8 @@ def finditer(
JSONPathSyntaxError: If the path is invalid.
JSONPathTypeError: If a filter expression attempts to use types in
an incompatible way.
JSONPathRecursionError: If `max_recursion_depth` is reached with
the descendent segment.
"""
_data = load_data(data)
path = self.env.pseudo_root_token if self.pseudo_root else self.env.root_token
Expand Down Expand Up @@ -189,6 +193,8 @@ def match(
JSONPathSyntaxError: If the path is invalid.
JSONPathTypeError: If a filter expression attempts to use types in
an incompatible way.
JSONPathRecursionError: If `max_recursion_depth` is reached with
the descendent segment.
"""
try:
return next(iter(self.finditer(data, filter_context=filter_context)))
Expand All @@ -213,6 +219,8 @@ def query(
JSONPathSyntaxError: If the path is invalid.
JSONPathTypeError: If a filter expression attempts to use types in
an incompatible way.
JSONPathRecursionError: If `max_recursion_depth` is reached with
the descendent segment.
"""
return Query(self.finditer(data, filter_context=filter_context), self.env)

Expand Down Expand Up @@ -291,6 +299,9 @@ def findall(
JSONPathSyntaxError: If the path is invalid.
JSONPathTypeError: If a filter expression attempts to use types in
an incompatible way.
JSONPathRecursionError: If `max_recursion_depth` is reached with
the descendent segment.

"""
objs = self.path.findall(data, filter_context=filter_context)

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

Expand Down Expand Up @@ -360,6 +373,8 @@ def match(
JSONPathSyntaxError: If the path is invalid.
JSONPathTypeError: If a filter expression attempts to use types in
an incompatible way.
JSONPathRecursionError: If `max_recursion_depth` is reached with
the descendent segment.
"""
try:
return next(iter(self.finditer(data, filter_context=filter_context)))
Expand Down Expand Up @@ -417,6 +432,8 @@ def query(
JSONPathSyntaxError: If the path is invalid.
JSONPathTypeError: If a filter expression attempts to use types in
an incompatible way.
JSONPathRecursionError: If `max_recursion_depth` is reached with
the descendent segment.
"""
return Query(self.finditer(data, filter_context=filter_context), self.env)

Expand Down
8 changes: 8 additions & 0 deletions tests/test_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,11 @@ class MockEnv(JSONPathEnvironment):

with pytest.raises(JSONPathRecursionError):
env.findall(query, data)


def test_compile_time_recursion_error(env: JSONPathEnvironment) -> None:
with pytest.raises(JSONPathRecursionError):
env.compile("$[?" + "!" * 493 + "@.a]")

with pytest.raises(RecursionError):
env.compile("$[?" + "!" * 493 + "@.a]")
Loading