Merge pull request #2116 from profanity-im/sanitzexml

feat: Sanitize illegal XML characters from outgoing messages
This commit is contained in:
Michael Vetter
2026-03-19 10:40:19 +01:00
committed by GitHub
6 changed files with 68 additions and 3 deletions

View File

@@ -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)
{

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);
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);

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);
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);

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

View File

@@ -669,6 +669,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,