Skip to content

Commit

Permalink
✨ decode: add strict_dept option (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
techouse authored Aug 12, 2024
1 parent e92b1ff commit 352a731
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 3 deletions.
18 changes: 17 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,24 @@ This depth can be overridden by setting the `depth <https://techouse.github.io/q
qs.DecodeOptions(depth=1),
) == {'a': {'b': {'[c][d][e][f][g][h][i]': 'j'}}}
You can configure `decode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.decode>`__ to throw an error
when parsing nested input beyond this depth using `strict_depth <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.strict_depth>`__ (defaults to ``False``):

.. code:: python
import qs_codec as qs
try:
qs.decode(
'a[b][c][d][e][f][g][h][i]=j',
qs.DecodeOptions(depth=1, strict_depth=True),
)
except IndexError as e:
assert str(e) == 'Input depth exceeded depth option of 1 and strict_depth is True'
The depth limit helps mitigate abuse when `decode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.decode>`__ is used to parse user
input, and it is recommended to keep it a reasonably small number.
input, and it is recommended to keep it a reasonably small number. `strict_depth <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.strict_depth>`__
adds a layer of protection by throwing a ``IndexError`` when the limit is exceeded, allowing you to catch and handle such cases.

For similar reasons, by default `decode <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.decode>`__ will only parse up to 1000 parameters. This can be overridden by passing a
`parameter_limit <https://techouse.github.io/qs_codec/qs_codec.models.html#qs_codec.models.decode_options.DecodeOptions.parameter_limit>`__ option:
Expand Down
21 changes: 19 additions & 2 deletions docs/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,25 @@ This depth can be overridden by setting the :py:attr:`depth <qs_codec.models.dec
qs.DecodeOptions(depth=1),
) == {'a': {'b': {'[c][d][e][f][g][h][i]': 'j'}}}
The depth limit helps mitigate abuse when :py:attr:`decode <qs_codec.decode>` is used to parse user
input, and it is recommended to keep it a reasonably small number.
You can configure :py:attr:`decode <qs_codec.decode>` to throw an error
when parsing nested input beyond this depth using :py:attr:`strict_depth <qs_codec.models.decode_options.DecodeOptions.strict_depth>` (defaults to ``False``):

.. code:: python
import qs_codec as qs
try:
qs.decode(
'a[b][c][d][e][f][g][h][i]=j',
qs.DecodeOptions(depth=1, strict_depth=True),
)
except IndexError as e:
assert str(e) == 'Input depth exceeded depth option of 1 and strict_depth is True'
The depth limit helps mitigate abuse when :py:attr:`decode <qs_codec.decode>` is used to parse user input, and it is recommended
to keep it a reasonably small number. :py:attr:`strict_depth <qs_codec.models.decode_options.DecodeOptions.strict_depth>`
adds a layer of protection by throwing a ``IndexError`` when the limit is exceeded, allowing you to catch and handle such cases.

For similar reasons, by default :py:attr:`decode <qs_codec.decode>` will only parse up to 1000 parameters. This can be overridden by passing a
:py:attr:`parameter_limit <qs_codec.models.decode_options.DecodeOptions.parameter_limit>` option:
Expand Down
2 changes: 2 additions & 0 deletions src/qs_codec/decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ def _parse_keys(given_key: t.Optional[str], val: t.Any, options: DecodeOptions,

# If there's a remainder, just add whatever is left
if segment is not None:
if options.strict_depth:
raise IndexError(f"Input depth exceeded depth option of {options.depth} and strict_depth is True")
keys.append(f"[{key[segment.start():]}]")

return _parse_object(keys, val, options, values_parsed)
3 changes: 3 additions & 0 deletions src/qs_codec/models/decode_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ class DecodeOptions:
parse_lists: bool = True
"""To disable ``list`` parsing entirely, set ``parse_lists`` to ``False``."""

strict_depth: bool = False
"""Set to ``True`` to throw an error when the input exceeds the ``depth`` limit."""

strict_null_handling: bool = False
"""Set to true to decode values without ``=`` to ``None``."""

Expand Down
36 changes: 36 additions & 0 deletions tests/unit/decode_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -642,3 +642,39 @@ def test_first(self) -> None:

def test_last(self) -> None:
assert decode("foo=bar&foo=baz", DecodeOptions(duplicates=Duplicates.LAST)) == {"foo": "baz"}


class TestStrictDepthOption:
def test_raises_index_error_for_multiple_nested_objects_with_strict_depth(self) -> None:
with pytest.raises(IndexError):
decode("a[b][c][d][e][f][g][h][i]=j", DecodeOptions(depth=1, strict_depth=True))

def test_raises_index_error_for_multiple_nested_lists_with_strict_depth(self) -> None:
with pytest.raises(IndexError):
decode("a[0][1][2][3][4]=b", DecodeOptions(depth=3, strict_depth=True))

def test_raises_index_error_for_nested_dicts_and_lists_with_strict_depth(self) -> None:
with pytest.raises(IndexError):
decode("a[b][c][0][d][e]=f", DecodeOptions(depth=3, strict_depth=True))

def test_raises_index_error_for_different_types_of_values_with_strict_depth(self) -> None:
with pytest.raises(IndexError):
decode("a[b][c][d][e]=true&a[b][c][d][f]=42", DecodeOptions(depth=3, strict_depth=True))

def test_when_depth_is_0_and_strict_depth_true_do_not_throw(self) -> None:
with does_not_raise():
decode("a[b][c][d][e]=true&a[b][c][d][f]=42", DecodeOptions(depth=0, strict_depth=True))

def test_decodes_successfully_when_depth_is_within_the_limit_with_strict_depth(self) -> None:
assert decode("a[b]=c", DecodeOptions(depth=1, strict_depth=True)) == {"a": {"b": "c"}}

def test_does_not_throw_an_exception_when_depth_exceeds_the_limit_with_strict_depth_false(self) -> None:
assert decode("a[b][c][d][e][f][g][h][i]=j", DecodeOptions(depth=1)) == {
"a": {"b": {"[c][d][e][f][g][h][i]": "j"}}
}

def test_decodes_successfully_when_depth_is_within_the_limit_with_strict_depth_false(self) -> None:
assert decode("a[b]=c", DecodeOptions(depth=1)) == {"a": {"b": "c"}}

def test_does_not_throw_when_depth_is_exactly_at_the_limit_with_strict_depth_true(self) -> None:
assert decode("a[b][c]=d", DecodeOptions(depth=2, strict_depth=True)) == {"a": {"b": {"c": "d"}}}

0 comments on commit 352a731

Please sign in to comment.