From 7404e7092a3abf83cdc274e6a2e5c8afa7339e21 Mon Sep 17 00:00:00 2001 From: "jabber.developer2" Date: Fri, 5 Jun 2026 14:49:09 +0300 Subject: [PATCH] fix: more issue #112 follow-ups (stanza-id disco gate, account sanitizer) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - xmpp: gate XEP-0359 stanza-id dedup on disco urn:xmpp:sid:0. Before honoring for archive_id dedup, check that the entity in `by` (or its domain — that's what profanity disco's on connect) announces the namespace; fall back to no-dedup when caps are unknown, matching the XEP §6 disco-gate (#1). - accounts: narrow group-name sanitizer to only the characters GKeyFile actually forbids in group headers (`[`, `]`, `\n`, `\r`); `=` and `#` are valid inside a header and stay (#2). - functional tests cover both branches of the disco gate — replayed stanza-id is flagged as duplicate when sid:0 is announced, and not flagged when it isn't. Refs #112 --- src/config/accounts.c | 4 +- src/xmpp/message.c | 24 ++++++++- tests/functionaltests/functionaltests.c | 4 ++ tests/functionaltests/test_message.c | 67 +++++++++++++++++++++++++ tests/functionaltests/test_message.h | 2 + 5 files changed, 97 insertions(+), 4 deletions(-) diff --git a/src/config/accounts.c b/src/config/accounts.c index af6b901d..af5d34e4 100644 --- a/src/config/accounts.c +++ b/src/config/accounts.c @@ -43,8 +43,8 @@ _sanitize_account_name(const char* const name) gchar* sanitized = g_strdup(name); gchar* p = sanitized; while (*p) { - // GKeyFile special characters: [ ] = # \n \r - if (*p == '[' || *p == ']' || *p == '=' || *p == '#' || *p == '\n' || *p == '\r') { + // GKeyFile group header forbids these characters. + if (*p == '[' || *p == ']' || *p == '\n' || *p == '\r') { *p = '_'; } p++; diff --git a/src/xmpp/message.c b/src/xmpp/message.c index a7b2b058..c6040bf1 100644 --- a/src/xmpp/message.c +++ b/src/xmpp/message.c @@ -65,6 +65,7 @@ static void _handle_pubsub(xmpp_stanza_t* const stanza, xmpp_stanza_t* const eve static gboolean _handle_form(xmpp_stanza_t* const stanza); static gboolean _handle_jingle_message(xmpp_stanza_t* const stanza); static gboolean _should_ignore_based_on_silence(xmpp_stanza_t* const stanza); +static gboolean _stanza_id_by_trusted(const char* by); #ifdef HAVE_OMEMO static void _receive_omemo(xmpp_stanza_t* const stanza, ProfMessage* message); #endif @@ -1088,7 +1089,7 @@ _handle_groupchat(xmpp_stanza_t* const stanza) xmpp_stanza_t* stanzaidst = xmpp_stanza_get_child_by_name_and_ns(stanza, STANZA_NAME_STANZA_ID, STANZA_NS_STABLE_ID); if (stanzaidst) { const char* by = xmpp_stanza_get_attribute(stanzaidst, "by"); - if (by && (g_strcmp0(by, from_jid->barejid) == 0)) { + if (by && g_strcmp0(by, from_jid->barejid) == 0 && _stanza_id_by_trusted(by)) { stanzaid = (char*)xmpp_stanza_get_attribute(stanzaidst, STANZA_ATTR_ID); if (stanzaid) { message->stanzaid = strdup(stanzaid); @@ -1397,7 +1398,7 @@ _handle_chat(xmpp_stanza_t* const stanza, gboolean is_mam, gboolean is_carbon, c xmpp_stanza_t* stanzaidst = xmpp_stanza_get_child_by_name_and_ns(stanza, STANZA_NAME_STANZA_ID, STANZA_NS_STABLE_ID); if (stanzaidst) { const char* by = xmpp_stanza_get_attribute(stanzaidst, "by"); - if (by && equals_our_barejid(by)) { + if (by && equals_our_barejid(by) && _stanza_id_by_trusted(by)) { stanzaid = (char*)xmpp_stanza_get_attribute(stanzaidst, STANZA_ATTR_ID); if (stanzaid) { message->stanzaid = strdup(stanzaid); @@ -1735,3 +1736,22 @@ _should_ignore_based_on_silence(xmpp_stanza_t* const stanza) } return FALSE; } + +// XEP-0359 §6 disco-gate; falls back to domain since that's what disco is run against. +static gboolean +_stanza_id_by_trusted(const char* by) +{ + if (!by) { + return FALSE; + } + GHashTable* features = connection_get_features(by); + if (features && g_hash_table_contains(features, STANZA_NS_STABLE_ID)) { + return TRUE; + } + const char* at = strchr(by, '@'); + if (!at) { + return FALSE; + } + features = connection_get_features(at + 1); + return features && g_hash_table_contains(features, STANZA_NS_STABLE_ID); +} diff --git a/tests/functionaltests/functionaltests.c b/tests/functionaltests/functionaltests.c index 0ea473c3..8102f928 100644 --- a/tests/functionaltests/functionaltests.c +++ b/tests/functionaltests/functionaltests.c @@ -253,6 +253,10 @@ main(int argc, char* argv[]) PROF_FUNC_TEST(message_receive_console), PROF_FUNC_TEST(message_receive_chatwin), + /* XEP-0359 disco gate for stanza-id trust */ + PROF_FUNC_TEST(stanza_id_dedup_fires_when_server_announces_sid0), + PROF_FUNC_TEST(stanza_id_not_trusted_when_server_does_not_announce_sid0), + #ifdef HAVE_SQLITE /* MUC (groupchat) database — export/import of type="muc" messages */ PROF_FUNC_TEST(muc_export_sqlite_to_flatfile), diff --git a/tests/functionaltests/test_message.c b/tests/functionaltests/test_message.c index 74f5e6c3..402144ac 100644 --- a/tests/functionaltests/test_message.c +++ b/tests/functionaltests/test_message.c @@ -56,3 +56,70 @@ message_receive_chatwin(void **state) assert_true(prof_output_regex("someuser@chatserv.org/laptop: .+How are you?")); } + +// XEP-0359 disco gate: server announces urn:xmpp:sid:0 -> stanza-id trusted -> replay flagged as duplicate. +void +stanza_id_dedup_fires_when_server_announces_sid0(void **state) +{ + stbbr_for_query("http://jabber.org/protocol/disco#info", + "" + "" + "" + "" + "" + "" + ); + + prof_connect(); + + stbbr_send( + "" + "first" + "" + "" + ); + assert_true(prof_output_exact("<< chat message: someuser@chatserv.org/laptop (win 2)")); + + stbbr_send( + "" + "replay" + "" + "" + ); + assert_true(prof_output_exact("Got a message with duplicate (server-generated) stanza-id from someuser@chatserv.org/laptop.")); +} + +// XEP-0359 disco gate: server does NOT announce urn:xmpp:sid:0 -> stanza-id untrusted -> no replay error. +void +stanza_id_not_trusted_when_server_does_not_announce_sid0(void **state) +{ + stbbr_for_query("http://jabber.org/protocol/disco#info", + "" + "" + "" + "" + "" + "" + ); + + prof_connect(); + + stbbr_send( + "" + "first" + "" + "" + ); + assert_true(prof_output_exact("<< chat message: someuser@chatserv.org/laptop (win 2)")); + + stbbr_send( + "" + "replay" + "" + "" + ); + + prof_timeout(2); + assert_false(prof_output_exact("Got a message with duplicate (server-generated) stanza-id")); + prof_timeout_reset(); +} diff --git a/tests/functionaltests/test_message.h b/tests/functionaltests/test_message.h index d3a1987c..924ad914 100644 --- a/tests/functionaltests/test_message.h +++ b/tests/functionaltests/test_message.h @@ -1,3 +1,5 @@ void message_send(void **state); void message_receive_console(void **state); void message_receive_chatwin(void **state); +void stanza_id_dedup_fires_when_server_announces_sid0(void **state); +void stanza_id_not_trusted_when_server_does_not_announce_sid0(void **state);