From 7b938b4d14aed5375745b8c72789afabfad4df25 Mon Sep 17 00:00:00 2001
From: Max Kellermann <mk@cm4all.com>
Date: Tue, 18 Jun 2024 17:20:06 +0200
Subject: [PATCH] util/Exception: sanitize message strings

This should prevent leaking unsanitized strings from libraries.
---
 src/util/Exception.cxx      | 38 ++++++++++++++++++++++++++++++++++---
 test/util/TestException.cxx | 18 ++++++++++++++++++
 2 files changed, 53 insertions(+), 3 deletions(-)

diff --git a/src/util/Exception.cxx b/src/util/Exception.cxx
index 351d5b7f5..17eff34df 100644
--- a/src/util/Exception.cxx
+++ b/src/util/Exception.cxx
@@ -2,9 +2,40 @@
 // author: Max Kellermann <max.kellermann@gmail.com>
 
 #include "Exception.hxx"
+#include "CharUtil.hxx"
+#include "StringStrip.hxx"
 
 #include <utility>
 
+/**
+ * Append the given C string to a std::string, with some
+ * sanitizations: strip whitespace at the beginning and the end and
+ * combine multiple whitespace to a single space.
+ *
+ * This shall compress strangely formatted multi-line messages (which
+ * may come from third-party libraries) to a single line.
+ */
+static void
+AppendSanitize(std::string &dest, const char *src) noexcept
+{
+	src = StripLeft(src);
+
+	bool space = false;
+	while (char ch = *src++) {
+		if (IsWhitespaceFast(ch)) {
+			space = true;
+			continue;
+		}
+
+		if (space) {
+			space = false;
+			dest.push_back(' ');
+		}
+
+		dest.push_back(ch);
+	}
+}
+
 template<typename T>
 static void
 AppendNestedMessage(std::string &result, T &&e,
@@ -14,13 +45,13 @@ AppendNestedMessage(std::string &result, T &&e,
 		std::rethrow_if_nested(std::forward<T>(e));
 	} catch (const std::exception &nested) {
 		result += separator;
-		result += nested.what();
+		AppendSanitize(result, nested.what());
 		AppendNestedMessage(result, nested, fallback, separator);
 	} catch (const std::nested_exception &ne) {
 		AppendNestedMessage(result, ne, fallback, separator);
 	} catch (const char *s) {
 		result += separator;
-		result += s;
+		AppendSanitize(result, s);
 	} catch (...) {
 		result += separator;
 		result += fallback;
@@ -31,7 +62,8 @@ std::string
 GetFullMessage(const std::exception &e,
 	       const char *fallback, const char *separator) noexcept
 {
-	std::string result = e.what();
+	std::string result;
+	AppendSanitize(result, e.what());
 	AppendNestedMessage(result, e, fallback, separator);
 	return result;
 }
diff --git a/test/util/TestException.cxx b/test/util/TestException.cxx
index c5dda7fd5..6e3e706e5 100644
--- a/test/util/TestException.cxx
+++ b/test/util/TestException.cxx
@@ -6,6 +6,8 @@
 
 #include <gtest/gtest.h>
 
+using std::string_view_literals::operator""sv;
+
 TEST(ExceptionTest, RuntimeError)
 {
 	ASSERT_EQ(GetFullMessage(std::make_exception_ptr(std::runtime_error("Foo"))), "Foo");
@@ -22,6 +24,22 @@ TEST(ExceptionTest, DerivedError)
 	ASSERT_EQ(GetFullMessage(std::make_exception_ptr(DerivedError("Foo"))), "Foo");
 }
 
+TEST(ExceptionTest, GetFullMessageSanitize)
+{
+	try {
+		try {
+			throw " ABC \n DEF ";
+		} catch (...) {
+			std::throw_with_nested(std::runtime_error{"foo\r\n\tbar"});
+		}
+
+		FAIL();
+	} catch (...) {
+		ASSERT_EQ(GetFullMessage(std::current_exception()),
+			  "foo bar; ABC DEF"sv);
+	}
+}
+
 TEST(ExceptionTest, FindNestedDirect)
 {
 	struct Foo {};