Skip to content

Commit b8d6bf1

Browse files
committed
escape NUL characters in EventLogAppender content #315
ReportEventW takes a null terminated string, so a NUL in logged content ended the stored record there and dropped whatever the layout rendered after it. WriteEntry raises nothing, so the record simply stored short and no ErrorHandler call fired. Measured on Windows 11 build 26200: a 45 character message with a NUL at 23 stored as its 23 character prefix. Escaping happens before the size limit is applied, since it doubles each NUL, and PrepareEventText exists so that ordering can be tested without an event log.
1 parent 8a30b9c commit b8d6bf1

3 files changed

Lines changed: 72 additions & 8 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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 `EventLogAppender` content. `ReportEventW` takes a null terminated
9+
string, so a NUL in logged content ended the stored record there and silently dropped whatever
10+
the layout rendered after it, exception text and trailing fields included (CWE-158). `WriteEntry`
11+
raises nothing, so the record simply stored short. Measured on Windows 11 build 26200: of a 45
12+
character message with a NUL at 23, the 23 character prefix was stored and the rest was gone
13+
(audit da18b6fd-f007)
14+
</description>
15+
</entry>

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

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#if NET462_OR_GREATER
2222

2323
using System.Diagnostics;
24+
using System.Reflection;
2425

2526
using log4net.Appender;
2627
using log4net.Core;
@@ -81,6 +82,40 @@ public void ActivateOptionsDisablesAppenderIfSourceDoesntExist()
8182
eventAppender.ActivateOptions();
8283
Assert.That(eventAppender.Threshold, Is.EqualTo(Level.Off));
8384
}
85+
86+
/// <summary>
87+
/// ReportEventW takes a null terminated string, so a NUL in content ends the stored record
88+
/// there and silently drops whatever the layout rendered after it. Measured on Windows 11
89+
/// 26200: WriteEntry does not throw, and only the prefix is stored.
90+
/// </summary>
91+
[Test]
92+
public void NulCharactersAreEscaped()
93+
=> Assert.That(PrepareEventText("before\0after", 100), Is.EqualTo("before\\0after"));
94+
95+
/// <summary>
96+
/// The escape doubles each NUL, so it has to happen before the limit is applied. Escaping
97+
/// afterwards would push a message near the limit back over it.
98+
/// </summary>
99+
[Test]
100+
public void EscapingHappensBeforeTheLimitIsApplied()
101+
{
102+
const int maxSize = 4;
103+
104+
string prepared = PrepareEventText("\0\0\0", maxSize);
105+
106+
Assert.That(prepared, Has.Length.EqualTo(maxSize));
107+
Assert.That(prepared, Does.Not.Contain("\0"));
108+
}
109+
110+
/// <summary>A message within the limit and without a NUL comes through untouched.</summary>
111+
[Test]
112+
public void MessagesWithinTheLimitAreUnchanged()
113+
=> Assert.That(PrepareEventText("field=1\tfield=2", 100), Is.EqualTo("field=1\tfield=2"));
114+
115+
private static string PrepareEventText(string rendered, int maxSize)
116+
=> (string)typeof(EventLogAppender)
117+
.GetMethod("PrepareEventText", BindingFlags.Static | BindingFlags.NonPublic)!
118+
.Invoke(null, [rendered, maxSize])!;
84119
}
85120

86-
#endif // NET462_OR_GREATER
121+
#endif // NET462_OR_GREATER

‎src/log4net/Appender/EventLogAppender.cs‎

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
using System.Diagnostics;
2424

2525
using log4net.Util;
26+
using log4net.Appender.Internal;
2627
using log4net.Core;
2728

2829
namespace log4net.Appender;
@@ -377,13 +378,7 @@ protected override void Append(LoggingEvent loggingEvent)
377378
// Write to the event log
378379
try
379380
{
380-
string eventTxt = RenderLoggingEvent(loggingEvent);
381-
382-
// There is a limit of about 32K characters for an event log message
383-
if (eventTxt.Length > _maxEventlogMessageSize)
384-
{
385-
eventTxt = eventTxt.Substring(0, _maxEventlogMessageSize);
386-
}
381+
string eventTxt = PrepareEventText(RenderLoggingEvent(loggingEvent), _maxEventlogMessageSize);
387382

388383
EventLogEntryType entryType = GetEntryType(loggingEvent.Level);
389384

@@ -398,6 +393,25 @@ protected override void Append(LoggingEvent loggingEvent)
398393
}
399394
}
400395

396+
/// <summary>
397+
/// Escapes the NUL characters and then applies the message size limit.
398+
/// </summary>
399+
/// <param name="rendered">The rendered event.</param>
400+
/// <param name="maxSize">The largest message the event log accepts.</param>
401+
/// <returns>The text to write.</returns>
402+
/// <remarks>
403+
/// <para>
404+
/// The order matters. <c>ReportEventW</c> takes a null terminated string, so a NUL in content
405+
/// ends the stored record there, and escaping doubles each NUL, so escaping after the limit was
406+
/// applied could push the message back over it.
407+
/// </para>
408+
/// </remarks>
409+
private static string PrepareEventText(string rendered, int maxSize)
410+
{
411+
string escaped = NativeStringEscape.EscapeNulCharacters(rendered);
412+
return escaped.Length > maxSize ? escaped.Substring(0, maxSize) : escaped;
413+
}
414+
401415
/// <summary>
402416
/// This appender requires a <see cref="Layout"/> to be set.
403417
/// </summary>

0 commit comments

Comments
 (0)