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