From a283c4e7b49e8e905d899a1cbb45bfed67035c03 Mon Sep 17 00:00:00 2001 From: "jabber.developer2" Date: Tue, 21 Jul 2026 09:09:49 +0300 Subject: [PATCH] fix(xmpp): treat disco#info result without 'from' as from the server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RFC 6120 §8.1.2.1: a stanza received over a c2s stream without a 'from' attribute must be treated as coming from the server itself. The on-connect disco#info handler passed the absent attribute as NULL into connection_features_received(), where g_str_hash() dereferenced the NULL key and crashed (remotely triggerable DoS on connect). Substitute connection_get_domain() at both disco#info handler boundaries, and make connection_features_received() and connection_get_features() NULL-safe as defense in depth. Add a stabber regression test answering the on-connect disco#info with a from-less result. Fixes #168 --- src/xmpp/connection.c | 12 ++++++++-- src/xmpp/iq.c | 2 ++ tests/functionaltests/functionaltests.c | 1 + tests/functionaltests/test_disco.c | 29 +++++++++++++++++++++++++ tests/functionaltests/test_disco.h | 1 + 5 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/xmpp/connection.c b/src/xmpp/connection.c index b0a8c7f4..1bce2876 100644 --- a/src/xmpp/connection.c +++ b/src/xmpp/connection.c @@ -753,7 +753,11 @@ void connection_features_received(const char* const jid) { log_info("[CONNECTION] connection_features_received %s", jid); - if (g_hash_table_remove(conn.requested_features, jid) && g_hash_table_size(conn.requested_features) == 0) { + const char* key = jid ? jid : conn.domain; // g_str_hash crashes on NULL; NULL 'from' means the server (RFC 6120 §8.1.2.1) + if (!key) { + return; + } + if (g_hash_table_remove(conn.requested_features, key) && g_hash_table_size(conn.requested_features) == 0) { sv_ev_connection_features_received(); } } @@ -761,7 +765,11 @@ connection_features_received(const char* const jid) GHashTable* connection_get_features(const char* const jid) { - return g_hash_table_lookup(conn.features_by_jid, jid); + const char* key = jid ? jid : conn.domain; + if (!key || !conn.features_by_jid) { + return NULL; + } + return g_hash_table_lookup(conn.features_by_jid, key); } GList* diff --git a/src/xmpp/iq.c b/src/xmpp/iq.c index 2a472bcf..1c9869f5 100644 --- a/src/xmpp/iq.c +++ b/src/xmpp/iq.c @@ -2314,6 +2314,7 @@ _disco_info_response_id_handler(xmpp_stanza_t* const stanza, void* const userdat log_debug("Received disco#info response from: %s", from); } else { log_debug("Received disco#info response"); + from = connection_get_domain(); // RFC 6120 §8.1.2.1: no 'from' means the server itself } // handle error responses @@ -2397,6 +2398,7 @@ _disco_info_response_id_handler_onconnect(xmpp_stanza_t* const stanza, void* con log_debug("Received disco#info response from: %s", from); } else { log_debug("Received disco#info response"); + from = connection_get_domain(); // RFC 6120 §8.1.2.1: no 'from' means the server itself } // handle error responses diff --git a/tests/functionaltests/functionaltests.c b/tests/functionaltests/functionaltests.c index 394b31d9..da68145e 100644 --- a/tests/functionaltests/functionaltests.c +++ b/tests/functionaltests/functionaltests.c @@ -173,6 +173,7 @@ main(int argc, char* argv[]) PROF_FUNC_TEST(disco_info_without_name), PROF_FUNC_TEST(disco_items_without_name), PROF_FUNC_TEST(disco_info_service_unavailable), + PROF_FUNC_TEST(disco_info_result_no_from), /* Roster management - add/remove/rename contacts */ PROF_FUNC_TEST(sends_new_item), diff --git a/tests/functionaltests/test_disco.c b/tests/functionaltests/test_disco.c index 1d565b59..822e822d 100644 --- a/tests/functionaltests/test_disco.c +++ b/tests/functionaltests/test_disco.c @@ -396,6 +396,35 @@ disco_items_without_name(void **state) prof_timeout_reset(); } +void +disco_info_result_no_from(void **state) +{ + /* + * Test that a disco#info result without a 'from' attribute is treated as + * coming from the server itself (RFC 6120 §8.1.2.1). The on-connect + * disco#info handler used to crash on such responses (issue #168). + */ + stbbr_for_query("http://jabber.org/protocol/disco#info", + "" + "" + "" + "" + "" + "" + ); + + /* the on-connect disco#info gets the same from-less response */ + prof_connect(); + + prof_input("/disco info"); + + prof_timeout(10); + /* client survived and attributed the response to the server */ + assert_true(prof_output_exact("Service discovery info for localhost")); + assert_true(prof_output_regex("NoFromServer.*im.*server")); + prof_timeout_reset(); +} + void disco_info_service_unavailable(void **state) { diff --git a/tests/functionaltests/test_disco.h b/tests/functionaltests/test_disco.h index 89a45c33..d37d03eb 100644 --- a/tests/functionaltests/test_disco.h +++ b/tests/functionaltests/test_disco.h @@ -17,3 +17,4 @@ void disco_info_multiple_identities(void **state); void disco_info_without_name(void **state); void disco_items_without_name(void **state); void disco_info_service_unavailable(void **state); +void disco_info_result_no_from(void **state);