diff --git a/claude_code_log/html/assistant_formatters.py b/claude_code_log/html/assistant_formatters.py index 3128a50c..02295cf9 100644 --- a/claude_code_log/html/assistant_formatters.py +++ b/claude_code_log/html/assistant_formatters.py @@ -108,7 +108,10 @@ def format_image_content(image: ImageContent) -> str: src = export_image(image, mode="embedded") if src is None: return "[Image]" - return f'image' + # Escape the src: export_image allowlists the media type and + # validates the base64, but the data: URL still must not be able to + # break out of the attribute (issue #277). + return f'image' def format_unknown_content(content: UnknownMessage) -> str: diff --git a/claude_code_log/html/renderer.py b/claude_code_log/html/renderer.py index 92900896..3c8c4d55 100644 --- a/claude_code_log/html/renderer.py +++ b/claude_code_log/html/renderer.py @@ -473,7 +473,10 @@ def _format_image(self, image: ImageContent) -> str: ) if src is None: return "[Image]" - return f'image' + # Escape the src: export_image allowlists the media type and + # validates the base64, but the data: URL (embedded mode) still + # must not be able to break out of the attribute (issue #277). + return f'image' # ------------------------------------------------------------------------- # System Content Formatters diff --git a/claude_code_log/image_export.py b/claude_code_log/image_export.py index 1113ac9a..089e2d58 100644 --- a/claude_code_log/image_export.py +++ b/claude_code_log/image_export.py @@ -13,6 +13,47 @@ from .models import ImageContent +# Image media types we are willing to emit into a data: URL or write to +# disk. Deliberately excludes ``image/svg+xml`` — SVG can carry inline +# ``') + assert export_image(hostile, mode="embedded") is None + + def test_embedded_rejects_svg(self): + """SVG is scriptable; embedded mode must not emit a data:image/svg+xml URL.""" + svg = _image("image/svg+xml") + assert export_image(svg, mode="embedded") is None + + def test_embedded_rejects_invalid_base64(self): + """Malformed base64 data is rejected rather than emitted verbatim.""" + bad = _image("image/png", data='not"base64>') + html = format_image_content(hostile) + assert "