From 7ad248f3a816ff280d19f876fea68617ec63b2b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 17:59:38 +0000 Subject: [PATCH] fix(security): neutralize NUL (0x00) in log records (CWE-117 gap) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CWE-117 formatter hardening merged via #1270 (now on main) covers the str.splitlines() boundary set + ESC/FS/GS/RS but omits NUL (0x00). The only cluster PR that carried NUL, #1255, was closed during consolidation without the codepoint being carried over — so main's StructuredFormatter lets a raw NUL reach the sink, where a C-based log shipper can truncate the record. Add ord("\x00"): "\\u0000" to _UNSAFE_LOG_CHARS (the exact fix the cluster consolidation analysis on #1270 specified) so NUL is escaped to a JSON-valid, reversible sequence like the other separators. Adds a focused regression test; the existing table-driven tests auto-extend to cover it. Focused suite: 10 passed. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01GSFzJYm5bYMuoAo9ssoiAM --- .../backend/config/logging_config.py | 4 +++- tests/unit/test_logging_config_crlf.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/youtube_extension/backend/config/logging_config.py b/src/youtube_extension/backend/config/logging_config.py index 3d061dd92..bd5eacec1 100644 --- a/src/youtube_extension/backend/config/logging_config.py +++ b/src/youtube_extension/backend/config/logging_config.py @@ -22,7 +22,8 @@ # # The set is the union of every separator ``str.splitlines()`` recognizes as a # line boundary (LF, CR, VT, FF, FS, GS, RS, NEL, LS, PS) plus ESC (terminal -# control sequences). Each is escaped to a JSON-valid ``\uXXXX`` sequence — not +# control sequences) and NUL (which can truncate a record inside a C-based log +# shipper). Each is escaped to a JSON-valid ``\uXXXX`` sequence — not # a Python ``\v``/``\x1b`` shorthand — so the neutralized record stays valid # JSON when ``enable_json_logging`` is on, while remaining a single physical # line for line-oriented sinks. @@ -33,6 +34,7 @@ # and the original text can be recovered by reversing the table. _UNSAFE_LOG_CHARS = { ord("\\"): "\\\\", + ord("\x00"): "\\u0000", # NUL — can truncate a record in a C-based log shipper ord("\n"): "\\u000a", ord("\r"): "\\u000d", ord("\v"): "\\u000b", # VT / 0x0B diff --git a/tests/unit/test_logging_config_crlf.py b/tests/unit/test_logging_config_crlf.py index 0dc17d72c..863bf35c6 100644 --- a/tests/unit/test_logging_config_crlf.py +++ b/tests/unit/test_logging_config_crlf.py @@ -120,6 +120,20 @@ def test_all_splitlines_boundaries_are_escaped(): assert cleaned.splitlines() == [cleaned] # collapses to a single line +def test_nul_byte_is_neutralized(): + # A NUL is not a splitlines() boundary, so line-oriented checks miss it, but + # it can truncate a record inside a C-based log shipper. It must be escaped + # to a JSON-valid sequence rather than reaching the sink raw. + assert ord("\x00") in _UNSAFE_LOG_CHARS + logger, buf = _make_logger("crlf-nul") + logger.info("user said: %s", "before\x00after") + out = buf.getvalue() + + assert "\x00" not in out # no raw NUL survives to the sink + assert "before\\u0000after" in out # escaped, and content preserved + assert json.loads(f'"{sanitize_log_record(chr(0))}"') == "\x00" # reversible + + def test_json_logging_output_stays_parseable(): # With enable_json_logging=True the record is interpolated into a JSON # string; the escapes must be JSON-valid so an attacker cannot corrupt or