fix(security): isolate XLSX decoding with hard resource limits
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user