feat: Sanitize illegal XML characters from outgoing messages

Filter out control characters (U+0000 to U+001F) from outgoing
messages, as they are illegal in XML 1.0 (except for \t, \n, and \r).
This prevents XMPP servers from closing the connection when such
characters are accidentally or intentionally included in a message.

Fixes: https://github.com/profanity-im/profanity/issues/1437
This commit is contained in:
Michael Vetter
2026-03-19 09:40:44 +01:00
parent 9de455ceea
commit 189050f3f4
6 changed files with 68 additions and 3 deletions

View File

@@ -418,6 +418,33 @@ utf8_display_len(const char* const str)
return len; 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* char*
release_get_latest(void) release_get_latest(void)
{ {

View File

@@ -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); 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); gboolean strtoi_range(const char* str, int* saveptr, int min, int max, char** err_msg);
int utf8_display_len(const char* const str); 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 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); gboolean valid_tls_policy_option(const char* is);

View File

@@ -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); 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; char* replace_id = NULL;
if (correct_last_msg) { 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) 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); 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; char* replace_id = NULL;
if (correct_last_msg) { 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); privwin_message_left_room(privwin);
} else { } else {
auto_char char* plugin_msg = plugins_pre_priv_message_send(privwin->fulljid, msg); 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_jid Jid* jidp = jid_create(privwin->fulljid);
auto_char char* id = message_send_private(privwin->fulljid, message, oob_url); auto_char char* id = message_send_private(privwin->fulljid, message, oob_url);

View File

@@ -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 // is is an empty string, one of the options is an empty string
assert_true(string_matches_one_of("Test", "", FALSE, "option1", "", "option2", NULL)); 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);
}

View File

@@ -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 valid_tls_policy_option__is__correct_for_various_inputs(void** state);
void get_mentions__tests__various(void** state); void get_mentions__tests__various(void** state);
void release_is_new__tests__various(void** state); void release_is_new__tests__various(void** state);
void str_xml_sanitize__strips_illegal_characters(void** state);
#endif #endif

View File

@@ -661,6 +661,7 @@ main(int argc, char* argv[])
cmocka_unit_test(prof_occurrences__tests__large_message), cmocka_unit_test(prof_occurrences__tests__large_message),
cmocka_unit_test(get_mentions__tests__various), cmocka_unit_test(get_mentions__tests__various),
cmocka_unit_test(release_is_new__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, cmocka_unit_test_setup_teardown(plugins_get_command_names__returns__no_commands,
load_preferences, load_preferences,