fix(security): isolate XLSX decoding with hard resource limits

This commit is contained in:
2026-09-08 07:47:18 +02:00
parent 33de5cac55
commit be16f8218e
4 changed files with 153 additions and 8 deletions
+28 -8
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import re
import unittest
from dataclasses import replace
from io import BytesIO
from unittest.mock import patch
from zipfile import ZipFile
@@ -40,14 +41,12 @@ def parse(payload: bytes):
class XlsxSafetyBoundsTests(unittest.TestCase):
def test_sparse_rows_cannot_bypass_limit_by_not_counting_as_data(self):
with patch.object(adapters, "MAX_FILE_ROWS", 3):
with self.assertRaisesRegex(TabularSourceValidationError, "row"):
parse(workbook_bytes(last_cell="A6"))
with self.assertRaisesRegex(TabularSourceValidationError, "row"):
parse(workbook_bytes(last_cell=f"A{adapters.MAX_FILE_ROWS + 2}"))
def test_forged_small_dimensions_cannot_hide_far_away_cells(self):
with patch.object(adapters, "MAX_FILE_ROWS", 3):
with self.assertRaisesRegex(TabularSourceValidationError, "row"):
parse(workbook_bytes(dimension="A1:A2", last_cell="A1000000"))
with self.assertRaisesRegex(TabularSourceValidationError, "row"):
parse(workbook_bytes(dimension="A1:A2", last_cell="A1000000"))
def test_forged_small_dimensions_cannot_hide_out_of_range_columns(self):
with self.assertRaisesRegex(TabularSourceValidationError, "column"):
@@ -61,10 +60,31 @@ class XlsxSafetyBoundsTests(unittest.TestCase):
self.assertEqual("Sheet", sheet)
def test_small_blank_gaps_and_exact_limit_remain_usable(self):
with patch.object(adapters, "MAX_FILE_ROWS", 3):
rows, _sheet = parse(workbook_bytes(last_cell="A4"))
for cell in ("A4", f"A{adapters.MAX_FILE_ROWS + 1}"):
rows, _sheet = parse(workbook_bytes(last_cell=cell))
self.assertEqual(({"name": "Ada"},), rows)
def test_workbook_is_parsed_in_a_fresh_child_not_the_parent(self):
with patch.object(adapters, "_parse_xlsx_content", side_effect=AssertionError("parent parser ran")):
rows, sheet = parse(workbook_bytes())
self.assertEqual(({"name": "Ada"},), rows)
self.assertEqual("Sheet", sheet)
def test_real_worker_timeout_fails_without_parent_fallback(self):
limits = replace(adapters.XLSX_PROCESS_LIMITS, wall_seconds=0.001)
with patch.object(adapters, "XLSX_PROCESS_LIMITS", limits), patch.object(
adapters, "_parse_xlsx_content", side_effect=AssertionError("parent parser ran")
):
with self.assertRaisesRegex(TabularSourceValidationError, "timeout"):
parse(workbook_bytes())
def test_child_validation_keeps_missing_sheet_message(self):
with self.assertRaisesRegex(TabularSourceValidationError, "worksheet 'Missing' was not found"):
adapters.parse_managed_tabular_content(
workbook_bytes(), filename="fixture.xlsx", content_type=None,
delimiter=",", sheet_name="Missing",
)
if __name__ == "__main__":
unittest.main()