Byebye
This commit is contained in:
parent
094c4d2274
commit
f63cb92b35
18 changed files with 593 additions and 886 deletions
122
tests/test_deep_nesting_dos.py
Normal file
122
tests/test_deep_nesting_dos.py
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
"""Regression tests for the deeply-nested-input denial-of-service fix.
|
||||
|
||||
Pathologically nested configuration (thousands of container levels) used to
|
||||
drive the recursive walkers in ``core`` -- or the underlying parser itself --
|
||||
past Python's stack limit, raising an uncaught ``RecursionError``. The fix
|
||||
bounds nesting depth and converts both cases into a clean, catchable
|
||||
``ConfigTooDeeplyNestedError``. These tests pin that behaviour and guard
|
||||
against regressions, while confirming normally-nested configs still process.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from jinjaturtle.core import (
|
||||
ConfigTooDeeplyNestedError,
|
||||
MAX_CONFIG_DEPTH,
|
||||
analyze_loops,
|
||||
flatten_config,
|
||||
generate_ansible_yaml,
|
||||
generate_jinja2_template,
|
||||
parse_config,
|
||||
)
|
||||
|
||||
|
||||
def _run_pipeline(content: str, suffix: str) -> None:
|
||||
with tempfile.NamedTemporaryFile(
|
||||
"w", suffix=suffix, delete=False, encoding="utf-8"
|
||||
) as tf:
|
||||
tf.write(content)
|
||||
path = Path(tf.name)
|
||||
try:
|
||||
fmt, parsed = parse_config(path)
|
||||
loops = analyze_loops(fmt, parsed)
|
||||
flat = flatten_config(fmt, parsed, loops)
|
||||
generate_jinja2_template(
|
||||
fmt, parsed, "role", original_text=content, loop_candidates=loops
|
||||
)
|
||||
generate_ansible_yaml("role", flat, loops)
|
||||
finally:
|
||||
path.unlink()
|
||||
|
||||
|
||||
def _deep_json_objects(depth: int) -> str:
|
||||
s = "0"
|
||||
for _ in range(depth):
|
||||
s = '{"a":' + s + "}"
|
||||
return s
|
||||
|
||||
|
||||
def _deep_json_arrays(depth: int) -> str:
|
||||
return "[" * depth + "1" + "]" * depth
|
||||
|
||||
|
||||
def _deep_xml(depth: int) -> str:
|
||||
s = "v"
|
||||
for _ in range(depth):
|
||||
s = f"<a>{s}</a>"
|
||||
return s
|
||||
|
||||
|
||||
def _deep_yaml(depth: int) -> str:
|
||||
s = "v"
|
||||
for _ in range(depth):
|
||||
s = "{a: " + s + "}"
|
||||
return s
|
||||
|
||||
|
||||
def _deep_toml(depth: int) -> str:
|
||||
return "a = " + "[" * depth + "1" + "]" * depth
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"content, suffix",
|
||||
[
|
||||
(_deep_json_objects(5000), ".json"),
|
||||
(_deep_json_arrays(100000), ".json"),
|
||||
(_deep_xml(5000), ".xml"),
|
||||
(_deep_yaml(3000), ".yaml"),
|
||||
(_deep_toml(2000), ".toml"),
|
||||
],
|
||||
)
|
||||
def test_deeply_nested_input_is_rejected_cleanly(content: str, suffix: str) -> None:
|
||||
# Must raise our typed error, and crucially must NOT raise RecursionError
|
||||
# (pytest would report that as an error, but be explicit about intent).
|
||||
with pytest.raises(ConfigTooDeeplyNestedError):
|
||||
_run_pipeline(content, suffix)
|
||||
|
||||
|
||||
def test_recursion_error_never_escapes() -> None:
|
||||
# Belt-and-suspenders: ensure a RecursionError is never what surfaces.
|
||||
content = _deep_yaml(6000)
|
||||
try:
|
||||
_run_pipeline(content, ".yaml")
|
||||
except ConfigTooDeeplyNestedError:
|
||||
pass
|
||||
except RecursionError: # pragma: no cover - this is the bug we fixed
|
||||
pytest.fail("RecursionError escaped instead of ConfigTooDeeplyNestedError")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"content, suffix",
|
||||
[
|
||||
('{"name":"web","port":8080,"tags":["a","b"]}', ".json"),
|
||||
("<config><host>db</host><port>5432</port></config>", ".xml"),
|
||||
("name: web\nport: 8080\nnested:\n a:\n b: 1\n", ".yaml"),
|
||||
('[section]\nkey = "value"\nnums = [1, 2, 3]\n', ".toml"),
|
||||
("[section]\nkey = value\n", ".ini"),
|
||||
],
|
||||
)
|
||||
def test_normal_configs_still_process(content: str, suffix: str) -> None:
|
||||
# No exception expected for ordinary, shallow configurations.
|
||||
_run_pipeline(content, suffix)
|
||||
|
||||
|
||||
def test_just_under_limit_is_accepted() -> None:
|
||||
# A structure comfortably under the cap must still process successfully.
|
||||
content = _deep_json_objects(MAX_CONFIG_DEPTH - 10)
|
||||
_run_pipeline(content, ".json")
|
||||
Loading…
Add table
Add a link
Reference in a new issue