From bba6bc5f763ec040a50efa915fd9e342b83da21d Mon Sep 17 00:00:00 2001 From: "jabber.developer2" Date: Tue, 31 Mar 2026 19:13:29 +0300 Subject: [PATCH] feat: sanitize illegal XML characters from outgoing messages (upstream 189050f3) --- .gitignore | 1 + src/common.c | 27 +++++++++++++++++++++++++++ src/common.h | 1 + src/event/client_events.c | 9 ++++++--- tests/unittests/test_common.c | 32 ++++++++++++++++++++++++++++++++ tests/unittests/test_common.h | 1 + tests/unittests/unittests.c | 1 + 7 files changed, 69 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 0559b357..c38eab72 100644 --- a/.gitignore +++ b/.gitignore @@ -114,3 +114,4 @@ coverage/ *.gcda *.gcov coverage.info +REVIEW.txt diff --git a/src/common.c b/src/common.c index a1418b01..50727e13 100644 --- a/src/common.c +++ b/src/common.c @@ -380,6 +380,33 @@ utf8_display_len(const char* const str) return len; } +/** + * Removes illegal XML 1.0 characters from a string. + * + * This function creates a new string that excludes characters in the range + * U+0000 to U+001F, except for U+0009 (TAB), U+000A (LF), and U+000D (CR). + */ +gchar* +str_xml_sanitize(const char* const str) +{ + if (str == NULL) { + return NULL; + } + + GString* sanitized = g_string_new_len(NULL, strlen(str)); + const char* curr = str; + + while (*curr != '\0') { + gunichar c = g_utf8_get_char(curr); + if ((c >= 0x20) || (c == 0x09) || (c == 0x0A) || (c == 0x0D)) { + g_string_append_unichar(sanitized, c); + } + curr = g_utf8_next_char(curr); + } + + return g_string_free(sanitized, FALSE); +} + char* release_get_latest(void) { diff --git a/src/common.h b/src/common.h index 0532e0d7..b02eac8c 100644 --- a/src/common.h +++ b/src/common.h @@ -180,6 +180,7 @@ gboolean copy_file(const char* const src, const char* const target, const gboole char* str_replace(const char* string, const char* substr, const char* replacement); gboolean strtoi_range(const char* str, int* saveptr, int min, int max, char** err_msg); int utf8_display_len(const char* const str); +gchar* str_xml_sanitize(const char* const str); char* release_get_latest(void); gboolean release_is_new(char* found_version); diff --git a/src/event/client_events.c b/src/event/client_events.c index 7b68bdc1..94bf838c 100644 --- a/src/event/client_events.c +++ b/src/event/client_events.c @@ -139,7 +139,8 @@ cl_ev_send_msg_correct(ProfChatWin* chatwin, const char* const msg, const char* gboolean request_receipt = prefs_get_boolean(PREF_RECEIPTS_REQUEST); auto_char char* plugin_msg = plugins_pre_chat_message_send(chatwin->barejid, msg); - const char* const message = plugin_msg ?: msg; + auto_gchar gchar* sanitized_msg = str_xml_sanitize(plugin_msg ?: msg); + const char* message = sanitized_msg; char* replace_id = NULL; if (correct_last_msg) { @@ -201,7 +202,8 @@ void cl_ev_send_muc_msg_corrected(ProfMucWin* mucwin, const char* const msg, const char* const oob_url, gboolean correct_last_msg) { auto_char char* plugin_msg = plugins_pre_room_message_send(mucwin->roomjid, msg); - const char* const message = plugin_msg ?: msg; + auto_gchar gchar* sanitized_msg = str_xml_sanitize(plugin_msg ?: msg); + const char* message = sanitized_msg; char* replace_id = NULL; if (correct_last_msg) { @@ -242,7 +244,8 @@ cl_ev_send_priv_msg(ProfPrivateWin* privwin, const char* const msg, const char* privwin_message_left_room(privwin); } else { auto_char char* plugin_msg = plugins_pre_priv_message_send(privwin->fulljid, msg); - const char* const message = plugin_msg ?: msg; + auto_gchar gchar* sanitized_msg = str_xml_sanitize(plugin_msg ?: msg); + const char* message = sanitized_msg; auto_jid Jid* jidp = jid_create(privwin->fulljid); auto_char char* id = message_send_private(privwin->fulljid, message, oob_url); diff --git a/tests/unittests/test_common.c b/tests/unittests/test_common.c index 11a9fe74..eb61b21a 100644 --- a/tests/unittests/test_common.c +++ b/tests/unittests/test_common.c @@ -846,3 +846,35 @@ prof_whole_occurrences_tests(void** state) g_slist_free(expected); expected = NULL; } + +void +str_xml_sanitize__strips_illegal_characters(void** state) +{ + // Test NULL input + assert_null(str_xml_sanitize(NULL)); + + // Test empty string + gchar* res1 = str_xml_sanitize(""); + assert_string_equal("", res1); + g_free(res1); + + // Test string with no illegal characters + gchar* res2 = str_xml_sanitize("Hello World! \t\n\r"); + assert_string_equal("Hello World! \t\n\r", res2); + g_free(res2); + + // Test string with illegal characters (0x16 is ^V, 0x01 is ^A) + gchar* res3 = str_xml_sanitize("Hello\x16World\x01!"); + assert_string_equal("HelloWorld!", res3); + g_free(res3); + + // Test string with mixed legal and illegal control characters + gchar* res4 = str_xml_sanitize("\x09Legal\x0BIllegal\x0ALegal\x1FIllegal\x0DLegal"); + assert_string_equal("\tLegalIllegal\nLegalIllegal\rLegal", res4); + g_free(res4); + + // Test UTF-8 characters + gchar* res5 = str_xml_sanitize("UTF-8: üñîçøðé \x16 and more"); + assert_string_equal("UTF-8: üñîçøðé and more", res5); + g_free(res5); +} diff --git a/tests/unittests/test_common.h b/tests/unittests/test_common.h index ffcc6c9d..d5007d89 100644 --- a/tests/unittests/test_common.h +++ b/tests/unittests/test_common.h @@ -34,3 +34,4 @@ void prof_whole_occurrences_tests(void** state); void prof_occurrences_of_large_message_tests(void** state); void unique_filename_from_url_td(void** state); void format_call_external_argv_td(void** state); +void str_xml_sanitize__strips_illegal_characters(void** state); diff --git a/tests/unittests/unittests.c b/tests/unittests/unittests.c index 69fbd4bf..bd5eee83 100644 --- a/tests/unittests/unittests.c +++ b/tests/unittests/unittests.c @@ -628,6 +628,7 @@ main(int argc, char* argv[]) cmocka_unit_test(prof_partial_occurrences_tests), cmocka_unit_test(prof_whole_occurrences_tests), cmocka_unit_test(prof_occurrences_of_large_message_tests), + cmocka_unit_test(str_xml_sanitize__strips_illegal_characters), cmocka_unit_test_setup_teardown(returns_no_commands, load_preferences,