Bound management JSON depth before decoding on every Python version

This commit is contained in:
mikecarper committed 2026-10-15 23:59:54 -07:00
1 parent 55373f692e
commit aa0a3832f4
2 files changed
+69 -2

No files matched your search

+41
View File
@@ -244,6 +244,47 @@ class ManagementTests(unittest.TestCase):
self.assertNotIn("Traceback", errors.getvalue())
password_prompt.assert_not_called()
def test_cli_rejects_deep_mqtt_jsonl_before_password_prompt(self):
path = Path(self.work.name) / "cli-deep-jsonl.json"
deep = '{"metadata":' * 10000 + "0" + "}" * 10000
for text in ("{}\n" + deep, deep + "\n{}"):
path.write_text(text, encoding="ascii")
output, errors = io.StringIO(), io.StringIO()
with mock.patch.object(report.getpass, "getpass") as password_prompt, \
mock.patch.object(sys, "argv", ["report.py", str(path), "--mqtt"]), \
contextlib.redirect_stdout(output), contextlib.redirect_stderr(errors):
with self.assertRaises(SystemExit) as failure:
report.main()
self.assertEqual(failure.exception.code, 2)
self.assertEqual(output.getvalue(), "")
self.assertIn("nested too deeply", errors.getvalue())
self.assertNotIn("Traceback", errors.getvalue())
password_prompt.assert_not_called()
def test_input_json_depth_limit_is_explicit_and_checks_before_decoding(self):
limit = report.MAX_JSON_DEPTH
for opening, closing in (("[", "]"), ('{"metadata":', "}")):
with self.subTest(opening=opening):
value = report._load_input_json(opening * limit + "0" + closing * limit)
for _ in range(limit):
value = value[0] if isinstance(value, list) else value["metadata"]
self.assertEqual(value, 0)
text = opening * (limit + 1) + "0" + closing * (limit + 1)
with mock.patch.object(report.json, "loads") as decoder:
with self.assertRaisesRegex(ValueError, "nested too deeply"):
report._load_input_json(text)
decoder.assert_not_called()
def test_input_json_depth_ignores_string_brackets_and_escaped_quotes(self):
text = 'brackets [{]} and quote " and backslash \\ ' * 1000
document = {"metadata": text, "raw": "\\"}
self.assertEqual(report._load_input_json(json.dumps(document)), document)
# An escaped backslash before a closing quote must not hide the next
# actual container, including when the following record exceeds depth.
deep = "[" * report.MAX_JSON_DEPTH + "0" + "]" * report.MAX_JSON_DEPTH
with self.assertRaisesRegex(ValueError, "nested too deeply"):
report._load_input_json("[" + json.dumps("\\") + "," + deep + "]")
def test_interoperable_full_report(self):
decoded = report.decode_report(list(reversed(self.pages)), "management test password")
self.assertEqual(decoded["sequence"], 42)
+28 -2
View File
@@ -15,6 +15,31 @@ from pathlib import Path
HEADER, ENTRY, PER_PAGE, TAG, MAX_KEYS = 83, 13, 6, 16, 36
CURRENT_HEADER, CURRENT_PER_PAGE = 111, 4
MAX_JSON_DEPTH = 64
def _load_input_json(text):
# Do not rely on the interpreter's JSON scanner using recursive calls.
# Check before decoding so excessive containers never become a deep tree.
depth = 0
quoted = escaped = False
for char in text:
if quoted:
if escaped:
escaped = False
elif char == "\\":
escaped = True
elif char == '"':
quoted = False
elif char == '"':
quoted = True
elif char in "[{":
depth += 1
if depth > MAX_JSON_DEPTH:
raise ValueError("input JSON is nested too deeply")
elif char in "]}":
depth -= 1
return json.loads(text)
def _layout(payload):
@@ -258,9 +283,10 @@ def main():
raise ValueError("administrator key must be 32 bytes")
text = args.pages.read_text(encoding="utf-8")
if args.mqtt:
messages = json.loads(text) if text.lstrip().startswith("[") else [json.loads(line) for line in text.splitlines() if line.strip()]
messages = (_load_input_json(text) if text.lstrip().startswith("[") else
[_load_input_json(line) for line in text.splitlines() if line.strip()])
else:
payloads = [bytes.fromhex(p) for p in json.loads(text)]
payloads = [bytes.fromhex(p) for p in _load_input_json(text)]
password = getpass.getpass("Management password: ")
if args.mqtt:
reports = mqtt_reports(messages, password)