Description: CVE-2025-54812 - part 1 - Improper HTML escaping in HTMLLayout
Origin: https://github.com/apache/logging-log4cxx/commit/1c599de956ae9eedd8b5e3f744bfb867c39e8bba
Bug: https://logging.apache.org/security.html#CVE-2025-54812
Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1111879

From 1c599de956ae9eedd8b5e3f744bfb867c39e8bba Mon Sep 17 00:00:00 2001
From: Stephen Webb <stephen.webb@ieee.org>
Date: Sat, 12 Jul 2025 10:19:03 +1000
Subject: [PATCH] Escape any logger name '&' or '"' in html attribute data
 (#509)

---
 src/main/cpp/htmllayout.cpp        |  8 ++---
 src/test/cpp/xml/xmllayouttest.cpp | 49 ++++++++++++++++++++++++++++++
 2 files changed, 53 insertions(+), 4 deletions(-)

diff --git a/src/main/cpp/htmllayout.cpp b/src/main/cpp/htmllayout.cpp
index c2411ece9..ed6e61d76 100644
--- a/src/main/cpp/htmllayout.cpp
+++ b/src/main/cpp/htmllayout.cpp
@@ -109,25 +109,25 @@ void HTMLLayout::format(LogString& output,
 	if (event->getLevel()->equals(Level::getDebug()))
 	{
 		output.append(LOG4CXX_STR("<font color=\"#339933\">"));
-		output.append(event->getLevel()->toString());
+		Transform::appendEscapingTags(output, event->getLevel()->toString());
 		output.append(LOG4CXX_STR("</font>"));
 	}
 	else if (event->getLevel()->isGreaterOrEqual(Level::getWarn()))
 	{
 		output.append(LOG4CXX_STR("<font color=\"#993300\"><strong>"));
-		output.append(event->getLevel()->toString());
+		Transform::appendEscapingTags(output, event->getLevel()->toString());
 		output.append(LOG4CXX_STR("</strong></font>"));
 	}
 	else
 	{
-		output.append(event->getLevel()->toString());
+		Transform::appendEscapingTags(output, event->getLevel()->toString());
 	}
 
 	output.append(LOG4CXX_STR("</td>"));
 	output.append(LOG4CXX_EOL);
 
 	output.append(LOG4CXX_STR("<td title=\""));
-	output.append(event->getLoggerName());
+	Transform::appendEscapingTags(output, event->getLoggerName());
 	output.append(LOG4CXX_STR(" logger\">"));
 	Transform::appendEscapingTags(output, event->getLoggerName());
 	output.append(LOG4CXX_STR("</td>"));
diff --git a/src/test/cpp/xml/xmllayouttest.cpp b/src/test/cpp/xml/xmllayouttest.cpp
index 78f1c1183..710e9fca4 100644
--- a/src/test/cpp/xml/xmllayouttest.cpp
+++ b/src/test/cpp/xml/xmllayouttest.cpp
@@ -18,6 +18,7 @@
 #include "../logunit.h"
 #include <log4cxx/logger.h>
 #include <log4cxx/xml/xmllayout.h>
+#include <log4cxx/htmllayout.h>
 #include <log4cxx/fileappender.h>
 #include <log4cxx/mdc.h>
 
@@ -37,6 +38,7 @@
 #include "../xml/xlevel.h"
 #include <log4cxx/helpers/bytebuffer.h>
 #include <log4cxx/helpers/transcoder.h>
+#include <log4cxx/helpers/loglog.h>
 
 
 using namespace log4cxx;
@@ -67,6 +69,7 @@ LOGUNIT_CLASS(XMLLayoutTest)
 	LOGUNIT_TEST(testActivateOptions);
 	LOGUNIT_TEST(testProblemCharacters);
 	LOGUNIT_TEST(testNDCWithCDATA);
+	LOGUNIT_TEST(testHTMLLayout);
 	LOGUNIT_TEST_SUITE_END();
 
 
@@ -453,6 +456,52 @@ LOGUNIT_CLASS(XMLLayoutTest)
 		LOGUNIT_ASSERT_EQUAL(1, ndcCount);
 	}
 
+	/**
+	 * Tests problematic characters in multiple fields.
+	 * @throws Exception if parser can not be constructed or source is not a valid XML document.
+	 */
+	void testHTMLLayout()
+	{
+		LogString problemName = LOG4CXX_STR("com.example.bar<>&\"'");
+		auto level = std::make_shared<XLevel>(6000, problemName, 7);
+		NDC context(problemName);
+		auto event = std::make_shared<LoggingEvent>(problemName, level, problemName, LOG4CXX_LOCATION);
+
+		HTMLLayout layout;
+		Pool p;
+		LogString html(LOG4CXX_STR("<body>"));
+		layout.format(html, event, p);
+		html += LOG4CXX_STR("</body>");
+
+		LogLog::debug(html);
+		char backing[3000];
+		ByteBuffer buf(backing, sizeof(backing));
+		CharsetEncoderPtr encoder(CharsetEncoder::getUTF8Encoder());
+		LogString::const_iterator iter{ html.begin() };
+		encoder->encode(html, iter, buf);
+		LOGUNIT_ASSERT(iter == html.end());
+		buf.flip();
+		auto parser = apr_xml_parser_create(p.getAPRPool());
+		LOGUNIT_ASSERT(parser != 0);
+		auto stat = apr_xml_parser_feed(parser, buf.data(), buf.remaining());
+		LOGUNIT_ASSERT(stat == APR_SUCCESS);
+		apr_xml_doc* doc = 0;
+		stat = apr_xml_parser_done(parser, &doc);
+		LOGUNIT_ASSERT(doc != 0);
+		auto parsedResult = doc->root;
+		LOGUNIT_ASSERT(parsedResult != 0);
+
+		int childElementCount = 0;
+		for ( auto node = parsedResult->first_child
+		    ; node != NULL
+		    ; node = node->next)
+		{
+			++childElementCount;
+			LOGUNIT_ASSERT_EQUAL(std::string("tr"), std::string(node->name));
+		}
+		LOGUNIT_ASSERT(1 < childElementCount);
+	}
+
 };
 
 
