From 89b7955b8181c1d4172d3102b514d6860cdc509f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 23:07:35 +0000 Subject: [PATCH] Strip bare control characters from CSP report terminal output _sanitize_terminal() only removed ESC-prefixed ANSI sequences, so bare C0 controls (CR, LF, BS, BEL, ...), DEL, C1 controls (e.g. 0x9B 8-bit CSI) and a stray ESC in unauthenticated /__csp__ report fields reached the operator's terminal, allowing log lines to be overwritten or forged. Strip all remaining control characters and Unicode bidi overrides after the ANSI escape pass. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019b8mWRK1bS7PUszC6SQKGB --- mesop/server/static_file_serving.py | 14 +++++- mesop/server/static_file_serving_test.py | 64 ++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/mesop/server/static_file_serving.py b/mesop/server/static_file_serving.py index 7c520e746..2980cd07e 100644 --- a/mesop/server/static_file_serving.py +++ b/mesop/server/static_file_serving.py @@ -59,13 +59,23 @@ def noop(): re.VERBOSE, ) +# Matches control characters that are not part of an ESC-prefixed sequence but +# can still manipulate terminal output (e.g. CR/BS overwrite text, LF forges new +# log lines, 0x9B is an 8-bit CSI), plus Unicode bidi overrides which can +# visually reorder text. +_CONTROL_CHARS_RE = re.compile( + r"[\x00-\x1f\x7f-\x9f\u200e\u200f\u202a-\u202e\u2066-\u2069]" +) + def _sanitize_terminal(value: object) -> str: - # Prevent ANSI/VT100 terminal escape injection when logging + # Prevent terminal escape and control character injection when logging # attacker-controlled data (e.g. CSP report fields). if not isinstance(value, str): value = str(value) - return _ANSI_ESCAPE_RE.sub("", value) + value = _ANSI_ESCAPE_RE.sub("", value) + # Strip any remaining control characters (including a stray ESC). + return _CONTROL_CHARS_RE.sub("", value) def configure_static_file_serving( diff --git a/mesop/server/static_file_serving_test.py b/mesop/server/static_file_serving_test.py index 8d941268e..3b75bb5b0 100644 --- a/mesop/server/static_file_serving_test.py +++ b/mesop/server/static_file_serving_test.py @@ -97,6 +97,41 @@ def test_sanitize_terminal_keeps_plain_text(): assert _sanitize_terminal(payload) == payload +def test_sanitize_terminal_removes_bare_control_characters(): + # C0 controls (incl. CR/LF/BS/BEL), DEL, C1 controls (e.g. 8-bit CSI) and + # a stray ESC are not ESC-prefixed sequences but still affect the terminal. + for ch in [ + "\x00", + "\x07", + "\x08", + "\t", + "\n", + "\x0b", + "\x0c", + "\r", + "\x0e", + "\x0f", + "\x1b", + "\x7f", + "\x85", + "\x9b", + ]: + assert _sanitize_terminal(f"before{ch}after") == "beforeafter", repr(ch) + + +def test_sanitize_terminal_removes_bidi_overrides(): + rlo, lri, pdi = chr(0x202E), chr(0x2066), chr(0x2069) + payload = f"abc{rlo}def{lri}g{pdi}" + + assert _sanitize_terminal(payload) == "abcdefg" + + +def test_sanitize_terminal_keeps_non_ascii_text(): + payload = "https://例え.jp/café?q=ü" + + assert _sanitize_terminal(payload) == payload + + def test_sanitize_terminal_coerces_non_str_input(): assert _sanitize_terminal(123) == "123" @@ -132,6 +167,35 @@ def test_csp_report_sanitizes_ansi_escapes_in_output(capsys): assert "evil.example" in output +def test_csp_report_strips_control_characters_in_output(capsys): + app = Flask(__name__) + configure_static_file_serving( + app, + static_file_runfiles_base="unused", + disable_gzip_cache=True, + ) + client = app.test_client() + + response = client.post( + "/__csp__", + json={ + "csp-report": { + "document-uri": "https://example.com/page\nFAKE: forged line", + "blocked-uri": "https://evil.example/x\rINFO: server healthy\x08\x07", + "violated-directive": "connect-src", + } + }, + ) + + assert response.status_code == 204 + output = capsys.readouterr().out + for ch in ["\r", "\x08", "\x07"]: + assert ch not in output + assert "\nFAKE" not in output + assert "https://evil.example/xINFO: server healthy" in output + assert "/pageFAKE: forged line" in output + + if __name__ == "__main__": import pytest