fix(security): isolate template rendering with hard resource limits

This commit is contained in:
2026-09-08 07:47:18 +02:00
parent 1f61464fd6
commit 1b1e3e0f92
6 changed files with 229 additions and 1 deletions
+38
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import unittest
from dataclasses import replace
from types import SimpleNamespace
from unittest.mock import patch
@@ -47,6 +48,43 @@ class TemplateRenderLimitTests(unittest.TestCase):
with self.assertRaises(TemplateRenderError):
rendering._render_payload(definition, revision, request=request, items=[{}])
def test_fresh_worker_preserves_exact_text_bytes_and_page_count(self):
definition = SimpleNamespace(name="Example")
revision = SimpleNamespace(content_html=None, content_text="Hi {{name}}: {{flag}}", template_type="serial_letter", layout={}, output_profiles=[])
request = SimpleNamespace(output_format="text", parameters={"flag": True})
with patch.object(rendering, "_render_payload_content", side_effect=AssertionError("parent renderer ran")):
result = rendering._render_payload(definition, revision, request=request, items=[{"name": "Äda"}, {"name": "Grace"}])
self.assertEqual(("Hi Äda: true\n\n---\n\nHi Grace: true".encode(), "text/plain; charset=utf-8", 2), result)
def test_real_worker_timeout_fails_without_parent_fallback(self):
definition = SimpleNamespace(name="Example")
revision = SimpleNamespace(content_html=None, content_text="Hello", template_type="serial_letter", layout={}, output_profiles=[])
request = SimpleNamespace(output_format="text", parameters={})
limits = replace(rendering.RENDER_PROCESS_LIMITS, wall_seconds=0.001)
with patch.object(rendering, "RENDER_PROCESS_LIMITS", limits), patch.object(
rendering, "_render_payload_content", side_effect=AssertionError("parent renderer ran")
):
with self.assertRaisesRegex(TemplateRenderError, "timeout"):
rendering._render_payload(definition, revision, request=request, items=[{}])
def test_worker_html_bytes_match_existing_composition(self):
definition = SimpleNamespace(name="Letters <archive>")
revision = SimpleNamespace(content_html="<p>{{name}}: {{settings}}</p>", content_text=None, template_type="label_sheet", layout={"page_size": "A5", "columns": 2, "rows": 2}, output_profiles=[])
request = SimpleNamespace(output_format="html", parameters={"settings": {"b": True, "a": "ü"}})
items = [{"name": "<Ada>"}, {"name": "Grace & Co"}]
expected = rendering._render_payload_content(definition, revision, request=request, items=items)
actual = rendering._render_payload(definition, revision, request=request, items=items)
self.assertEqual(expected, actual)
def test_real_worker_output_transport_limit_is_enforced(self):
definition = SimpleNamespace(name="Example")
revision = SimpleNamespace(content_html=None, content_text="x" * 4096, template_type="serial_letter", layout={}, output_profiles=[])
request = SimpleNamespace(output_format="text", parameters={})
limits = replace(rendering.RENDER_PROCESS_LIMITS, output_bytes=1024)
with patch.object(rendering, "RENDER_PROCESS_LIMITS", limits):
with self.assertRaisesRegex(TemplateRenderError, "output_limit"):
rendering._render_payload(definition, revision, request=request, items=[{}])
if __name__ == "__main__":
unittest.main()