diff --git a/src/common.c b/src/common.c index 44c2fcff..eb1d1e8a 100644 --- a/src/common.c +++ b/src/common.c @@ -418,6 +418,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 0dccf9b5..cb3155e4 100644 --- a/src/common.h +++ b/src/common.h @@ -159,6 +159,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); gboolean string_matches_one_of(const char* what, const char* is, gboolean is_can_be_null, const char* first, ...) __attribute__((sentinel)); gboolean valid_tls_policy_option(const char* is); diff --git a/src/event/client_events.c b/src/event/client_events.c index 51883197..498863a6 100644 --- a/src/event/client_events.c +++ b/src/event/client_events.c @@ -115,7 +115,8 @@ cl_ev_send_msg_correct(ProfChatWin* chatwin, const char* const msg, const char* } 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) { @@ -177,7 +178,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) { @@ -218,7 +220,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 cd62c84d..6a7bbb9f 100644 --- a/tests/unittests/test_common.c +++ b/tests/unittests/test_common.c @@ -1281,3 +1281,35 @@ string_matches_one_of__tests__edge_cases(void** state) // is is an empty string, one of the options is an empty string assert_true(string_matches_one_of("Test", "", FALSE, "option1", "", "option2", 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 50ce1262..c3e97bf8 100644 --- a/tests/unittests/test_common.h +++ b/tests/unittests/test_common.h @@ -60,5 +60,6 @@ void string_matches_one_of__tests__edge_cases(void** state); void valid_tls_policy_option__is__correct_for_various_inputs(void** state); void get_mentions__tests__various(void** state); void release_is_new__tests__various(void** state); +void str_xml_sanitize__strips_illegal_characters(void** state); #endif diff --git a/tests/unittests/unittests.c b/tests/unittests/unittests.c index 1dede0e5..d3f76ca2 100644 --- a/tests/unittests/unittests.c +++ b/tests/unittests/unittests.c @@ -661,6 +661,7 @@ main(int argc, char* argv[]) cmocka_unit_test(prof_occurrences__tests__large_message), cmocka_unit_test(get_mentions__tests__various), cmocka_unit_test(release_is_new__tests__various), + cmocka_unit_test(str_xml_sanitize__strips_illegal_characters), cmocka_unit_test_setup_teardown(plugins_get_command_names__returns__no_commands, load_preferences,