From 4f8be1c960746ad6a5d55899474ce96e712e1015 Mon Sep 17 00:00:00 2001 From: Nikhila-Dhevi-R <178061112+Nikhila-Dhevi-R@users.noreply.github.com> Date: Thu, 5 Nov 2026 09:24:21 +0530 Subject: [PATCH] fix: escape job output in notebook html --- packages/syft-job/src/syft_job/job_repr.py | 8 ++--- packages/syft-job/src/syft_job/job_stdout.py | 14 ++------ .../syft-job/tests/test_job_output_repr.py | 33 +++++++++++++++++++ 3 files changed, 38 insertions(+), 17 deletions(-) create mode 100644 packages/syft-job/tests/test_job_output_repr.py diff --git a/packages/syft-job/src/syft_job/job_repr.py b/packages/syft-job/src/syft_job/job_repr.py index accd0e90eeb..e396a33f315 100644 --- a/packages/syft-job/src/syft_job/job_repr.py +++ b/packages/syft-job/src/syft_job/job_repr.py @@ -121,12 +121,8 @@ def _repr_html_(self) -> str: """ - html_content = self._convert_ansi_to_html(raw_content) - html_content = ( - html_content.replace("&", "&") - .replace('"', """) - .replace("'", "'") - ) + # Escape job output before adding the trusted HTML generated from ANSI codes. + html_content = self._convert_ansi_to_html(escape(raw_content)) clean_content = self._strip_ansi_codes(raw_content) lines = clean_content.split("\n") diff --git a/packages/syft-job/src/syft_job/job_stdout.py b/packages/syft-job/src/syft_job/job_stdout.py index 64ca7bffbf8..caab0955efe 100644 --- a/packages/syft-job/src/syft_job/job_stdout.py +++ b/packages/syft-job/src/syft_job/job_stdout.py @@ -1,5 +1,6 @@ from __future__ import annotations +import html import re from typing import TYPE_CHECKING @@ -129,17 +130,8 @@ def _repr_html_(self) -> str: """ - # Convert ANSI codes to HTML for display - html_content = self._convert_ansi_to_html(raw_content) - - # Escape any remaining HTML characters that aren't our color spans - # We need to be careful not to escape our intentional HTML - html_content = ( - html_content.replace("&", "&") - .replace('"', """) - .replace("'", "'") - ) - # Don't escape < and > since we want our HTML spans to work + # Escape job output before adding the trusted HTML generated from ANSI codes. + html_content = self._convert_ansi_to_html(html.escape(raw_content)) # Count lines and characters (use clean content for stats) clean_content = self._strip_ansi_codes(raw_content) diff --git a/packages/syft-job/tests/test_job_output_repr.py b/packages/syft-job/tests/test_job_output_repr.py new file mode 100644 index 00000000000..9e5f0cb75c3 --- /dev/null +++ b/packages/syft-job/tests/test_job_output_repr.py @@ -0,0 +1,33 @@ +from types import SimpleNamespace + +import pytest + +from syft_job.job_repr import StderrViewer +from syft_job.job_stdout import StdoutViewer + + +@pytest.mark.parametrize( + ("viewer_class", "file_name"), + [ + (StdoutViewer, "stdout.txt"), + (StderrViewer, "stderr.txt"), + ], +) +def test_job_output_html_escapes_markup_and_preserves_ansi( + tmp_path, viewer_class, file_name +): + output_file = tmp_path / file_name + output_file.write_text( + "\n\x1b[31mfailed\x1b[0m\n", + encoding="utf-8", + ) + viewer = viewer_class( + SimpleNamespace(status="done", artifact_path=lambda _: output_file) + ) + + rendered_html = viewer._repr_html_() + + assert "<class 'int'>" in rendered_html + assert 'failed' in rendered_html + assert "