Skip to content

Commit dcbbef0

Browse files
committed
escape NUL characters in OutputDebugStringAppender content #315
OutputDebugStringW takes a null terminated string, so a NUL in logged content ended the record there and dropped whatever the layout rendered after it. The escape LocalSyslogAppender already had is now shared by both, since EventLogAppender is the same shape and will want it too. Its tests moved onto the shared helper with it. The appender level test only runs on Windows: Append refuses to run elsewhere.
1 parent 780f309 commit dcbbef0

6 files changed

Lines changed: 83 additions & 23 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xmlns="https://logging.apache.org/xml/ns"
4+
xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
5+
type="fixed">
6+
<issue id="315" link="https://github.com/apache/logging-log4net/pull/315"/>
7+
<description format="asciidoc">
8+
escape NUL characters in `OutputDebugStringAppender` content. `OutputDebugStringW` takes a null
9+
terminated string, so a NUL in logged content ended the record there and silently dropped whatever
10+
the layout rendered after it, exception text and trailing fields included (CWE-158). The escape
11+
`LocalSyslogAppender` already applied is now shared between the two (audit da18b6fd-f009)
12+
</description>
13+
</entry>

‎src/log4net.Tests/Appender/LocalSyslogAppenderTest.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ private static string[] SplitLines(string message)
156156
.Invoke(null, [message])!;
157157

158158
private static string EscapeNulCharacters(string message)
159-
=> (string)typeof(LocalSyslogAppender)
159+
=> (string)typeof(LocalSyslogAppender).Assembly
160+
.GetType("log4net.Appender.Internal.NativeStringEscape")!
160161
.GetMethod("EscapeNulCharacters", BindingFlags.Static | BindingFlags.NonPublic)!
161162
.Invoke(null, [message])!;
162163
}

‎src/log4net.Tests/Appender/OutputDebugAppenderTest.cs‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,29 @@ public void AppendShouldNotCauseAnyErrors()
5858
log.Debug(DebugMessage);
5959
Assert.That(lastDebugString, Is.Not.Null.And.Contains(DebugMessage));
6060
}
61+
62+
/// <summary>
63+
/// OutputDebugStringW takes a null terminated string, so a NUL in content would end the record
64+
/// there and drop whatever the layout rendered after it.
65+
/// </summary>
66+
[Test]
67+
public void NulCharactersAreEscapedBeforeTheNativeCall()
68+
{
69+
ILoggerRepository rep = LogManager.CreateRepository(Guid.NewGuid().ToString());
70+
string? lastDebugString = null;
71+
OutputAppender appender = new(value => lastDebugString = value)
72+
{
73+
Layout = new SimpleLayout(),
74+
ErrorHandler = new FailOnError()
75+
};
76+
appender.ActivateOptions();
77+
BasicConfigurator.Configure(rep, appender);
78+
79+
LogManager.GetLogger(rep.Name, GetType()).Debug("before\0after");
80+
81+
Assert.That(lastDebugString, Does.Contain("before\\0after"));
82+
Assert.That(lastDebugString, Does.Not.Contain("\0"));
83+
}
6184
}
6285

6386
file sealed class OutputAppender(Action<string> outputDebugString)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#region Apache License
2+
//
3+
// Licensed to the Apache Software Foundation (ASF) under one or more
4+
// contributor license agreements. See the NOTICE file distributed with
5+
// this work for additional information regarding copyright ownership.
6+
// The ASF licenses this file to you under the Apache License, Version 2.0
7+
// (the "License"); you may not use this file except in compliance with
8+
// the License. You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
#endregion
19+
20+
namespace log4net.Appender.Internal;
21+
22+
/// <summary>
23+
/// Prepares rendered content for a sink that takes a null terminated string.
24+
/// </summary>
25+
internal static class NativeStringEscape
26+
{
27+
/// <summary>
28+
/// Replaces NUL characters with a visible <c>\0</c> escape.
29+
/// </summary>
30+
/// <param name="message">The rendered message.</param>
31+
/// <returns>The message with every NUL character escaped.</returns>
32+
/// <remarks>
33+
/// <para>
34+
/// A NUL ends the string for the native sink, dropping everything the layout rendered after it,
35+
/// trailing fields and exception text included. Logged content is not trusted and may well
36+
/// contain a NUL, so the character is escaped rather than passed through.
37+
/// </para>
38+
/// </remarks>
39+
internal static string EscapeNulCharacters(string message)
40+
=> message.IndexOf('\0') < 0 ? message : message.Replace("\0", "\\0");
41+
}

‎src/log4net/Appender/LocalSyslogAppender.cs‎

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
using System;
2121
using System.Runtime.InteropServices;
2222

23+
using log4net.Appender.Internal;
2324
using log4net.Core;
2425
using log4net.Util;
2526

@@ -353,7 +354,7 @@ public override void ActivateOptions()
353354
protected override void Append(LoggingEvent loggingEvent)
354355
{
355356
int priority = GeneratePriority(Facility, GetSeverity(loggingEvent.EnsureNotNull().Level));
356-
string message = EscapeNulCharacters(RenderLoggingEvent(loggingEvent));
357+
string message = NativeStringEscape.EscapeNulCharacters(RenderLoggingEvent(loggingEvent));
357358

358359
// The second argument is a printf style format string.
359360
if (NewLineHandling == SyslogNewLineHandling.Split)
@@ -405,26 +406,6 @@ private static string[] SplitLines(string message)
405406

406407
private static readonly string[] _newLines = ["\r\n", "\n", "\r"];
407408

408-
/// <summary>
409-
/// Replaces NUL characters with a visible <c>\0</c> escape.
410-
/// </summary>
411-
/// <param name="message">The rendered message.</param>
412-
/// <returns>The message with every NUL character escaped.</returns>
413-
/// <remarks>
414-
/// <para>
415-
/// The message is marshaled to libc as a null-terminated string, so a NUL character anywhere in
416-
/// it would end the record there and silently drop everything the layout rendered after it,
417-
/// including trailing fields and exception text. Logged content is not trusted and may well
418-
/// contain a NUL, so the character is escaped rather than passed through.
419-
/// </para>
420-
/// <para>
421-
/// Newlines are handled separately, see <see cref="NewLineHandling"/>. Other control characters
422-
/// are passed through.
423-
/// </para>
424-
/// </remarks>
425-
private static string EscapeNulCharacters(string message)
426-
=> message.IndexOf('\0') < 0 ? message : message.Replace("\0", "\\0");
427-
428409
/// <summary>
429410
/// Close the syslog when the appender is closed
430411
/// </summary>

‎src/log4net/Appender/OutputDebugStringAppender.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#endregion
1919

2020
using System;
21+
using log4net.Appender.Internal;
2122
using log4net.Core;
2223
using log4net.Util;
2324

@@ -59,7 +60,7 @@ protected override void Append(LoggingEvent loggingEvent)
5960
}
6061
#endif
6162

62-
_outputDebugString(RenderLoggingEvent(loggingEvent));
63+
_outputDebugString(NativeStringEscape.EscapeNulCharacters(RenderLoggingEvent(loggingEvent)));
6364
}
6465

6566
/// <summary>

0 commit comments

Comments
 (0)