From b56c774103403e4bfb164afc5bff8ecfa50966df Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Wed, 22 Jul 2026 10:31:48 +0200 Subject: [PATCH] fix(logging): strip Unicode line separators in LogSanitizer The sanitizer matched \p{Cc}, which covers the C0/C1 control ranges but not U+2028 LINE SEPARATOR or U+2029 PARAGRAPH SEPARATOR - those are categories Zl/Zp. A log viewer, a JSON-lines consumer or any JavaScript-side reader still breaks a line on them, so they were a residual log-forging vector. Matching \p{Zl} and \p{Zp} rather than listing escapes: the two categories contain exactly those two characters, and it keeps the source free of invisible literals. Verified the test is not vacuous - the old pattern left "ab" as "a b", the new one yields "ab". Co-Authored-By: Claude Opus 4.8 (1M context) --- .../LogSanitizerTests.cs | 15 +++++++++++++++ .../Helpers/LogSanitizer.cs | 11 +++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/LogSanitizerTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/LogSanitizerTests.cs index 5df27fd4..1eb23f15 100644 --- a/src/Backend/AutopilotMonitor.Functions.Tests/LogSanitizerTests.cs +++ b/src/Backend/AutopilotMonitor.Functions.Tests/LogSanitizerTests.cs @@ -31,6 +31,21 @@ public class LogSanitizerTests Assert.Equal("abcde", LogSanitizer.Clean(input)); } + [Fact] + public void Clean_WithUnicodeLineSeparators_StripsThem() + { + // U+2028 LINE SEPARATOR and U+2029 PARAGRAPH SEPARATOR are Unicode categories + // Zl/Zp, not Cc — so a \p{Cc}-only pass would let them through, and a log viewer + // or JavaScript-side reader would still break the line on them. Built from char + // codes so the source file carries no literal separator bytes. + var forged = $"/api/sessions{(char)0x2028}[Auth Middleware] FAKE: admin granted"; + var cleaned = LogSanitizer.Clean(forged); + + Assert.Equal("/api/sessions[Auth Middleware] FAKE: admin granted", cleaned); + Assert.DoesNotContain((char)0x2028, cleaned!); + Assert.Equal("ab", LogSanitizer.Clean($"a{(char)0x2029}b")); + } + [Theory] [InlineData("/api/sessions/abc-123/events")] [InlineData("user@contoso.com")] diff --git a/src/Backend/AutopilotMonitor.Functions/Helpers/LogSanitizer.cs b/src/Backend/AutopilotMonitor.Functions/Helpers/LogSanitizer.cs index aa192d23..f5ad949e 100644 --- a/src/Backend/AutopilotMonitor.Functions/Helpers/LogSanitizer.cs +++ b/src/Backend/AutopilotMonitor.Functions/Helpers/LogSanitizer.cs @@ -14,11 +14,14 @@ namespace AutopilotMonitor.Functions.Helpers; public static class LogSanitizer { // \p{Cc} = Unicode "Other, control" (C0 + C1 ranges), which includes CR and LF. - private static readonly Regex ControlChars = new(@"\p{Cc}", RegexOptions.Compiled); + // \p{Zl} and \p{Zp} are the line and paragraph separators (U+2028 / U+2029). They + // are separators, not control characters, so \p{Cc} misses them — yet a log viewer, + // a JSON-lines consumer, or any JavaScript-side reader still breaks a line on them. + private static readonly Regex ControlChars = new(@"[\p{Cc}\p{Zl}\p{Zp}]", RegexOptions.Compiled); /// - /// Removes CR, LF, and every other control character from . - /// Returns null/empty inputs unchanged. + /// Removes CR, LF, the Unicode line/paragraph separators, and every other control + /// character from . Returns null/empty inputs unchanged. /// public static string? Clean(string? value) { @@ -26,7 +29,7 @@ public static class LogSanitizer return value; // Explicit CR/LF removal first — this is the log-injection barrier the CodeQL - // taint model recognizes; the regex pass then drops any remaining control chars. + // taint model recognizes; the regex pass then drops any remaining separators. var sanitized = value.Replace("\r", string.Empty).Replace("\n", string.Empty); return ControlChars.Replace(sanitized, string.Empty); }