diff --git a/src/logging.cpp b/src/logging.cpp
--- a/src/logging.cpp
+++ b/src/logging.cpp
@@ -204,9 +204,31 @@
     return strStamped;
 }
 
+namespace BCLog {
+/** Belts and suspenders: make sure outgoing log messages don't contain
+ * potentially suspicious characters, such as terminal control codes.
+ *
+ * This escapes control characters except newline ('\n') in C syntax.
+ * It escapes instead of removes them to still allow for troubleshooting
+ * issues where they accidentally end up in strings.
+ */
+std::string LogEscapeMessage(const std::string &str) {
+    std::string ret;
+    for (char ch_in : str) {
+        uint8_t ch = (uint8_t)ch_in;
+        if ((ch >= 32 || ch == '\n') && ch != '\x7f') {
+            ret += ch_in;
+        } else {
+            ret += strprintf("\\x%02x", ch);
+        }
+    }
+    return ret;
+}
+} // namespace BCLog
+
 void BCLog::Logger::LogPrintStr(const std::string &str) {
     std::lock_guard<std::mutex> scoped_lock(m_cs);
-    std::string str_prefixed = str;
+    std::string str_prefixed = LogEscapeMessage(str);
 
     if (m_log_threadnames && m_started_new_line) {
         str_prefixed.insert(0, "[" + util::ThreadGetInternalName() + "] ");
diff --git a/src/test/util_tests.cpp b/src/test/util_tests.cpp
--- a/src/test/util_tests.cpp
+++ b/src/test/util_tests.cpp
@@ -30,6 +30,11 @@
 #include <thread>
 #include <vector>
 
+/* defined in logging.cpp */
+namespace BCLog {
+std::string LogEscapeMessage(const std::string &str);
+}
+
 BOOST_FIXTURE_TEST_SUITE(util_tests, BasicTestingSetup)
 
 BOOST_AUTO_TEST_CASE(util_criticalsection) {
@@ -2193,4 +2198,20 @@
     BOOST_CHECK_EQUAL(SpanToStr(results[3]), "");
 }
 
+BOOST_AUTO_TEST_CASE(test_LogEscapeMessage) {
+    // ASCII and UTF-8 must pass through unaltered.
+    BOOST_CHECK_EQUAL(BCLog::LogEscapeMessage("Valid log message貓"),
+                      "Valid log message貓");
+    // Newlines must pass through unaltered.
+    BOOST_CHECK_EQUAL(BCLog::LogEscapeMessage("Message\n with newlines\n"),
+                      "Message\n with newlines\n");
+    // Other control characters are escaped in C syntax.
+    BOOST_CHECK_EQUAL(
+        BCLog::LogEscapeMessage("\x01\x7f Corrupted log message\x0d"),
+        R"(\x01\x7f Corrupted log message\x0d)");
+    // Embedded NULL characters are escaped too.
+    const std::string NUL("O\x00O", 3);
+    BOOST_CHECK_EQUAL(BCLog::LogEscapeMessage(NUL), R"(O\x00O)");
+}
+
 BOOST_AUTO_TEST_SUITE_END()