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