security: validate <delay> from and <stanza-id> by attributes
All checks were successful
CI Code / Check spelling (pull_request) Successful in 20s
CI Code / Check coding style (pull_request) Successful in 35s
CI Code / Code Coverage (pull_request) Successful in 4m47s
CI Code / Linux (debian) (pull_request) Successful in 6m8s
CI Code / Linux (ubuntu) (pull_request) Successful in 6m14s
CI Code / Linux (arch) (pull_request) Successful in 6m26s

XEP-0203 §4 (Delayed Delivery):
Remote clients could inject <delay xmlns='urn:xmpp:delay'> elements
with arbitrary timestamps, causing messages to appear at incorrect
positions in chat history.  The spec mandates checking the 'from'
attribute to verify the delay was added by a trusted source.

- stanza.c: add stanza_get_oldest_delay_from(stanza, from) that
  filters <delay>/<x> children by the 'from' attribute; refactor
  stanza_get_oldest_delay() to delegate with from=NULL.  Add
  NULL-safety for tmp timestamp in comparison logic.
- stanza.h: declare stanza_get_oldest_delay_from().
- message.c (_handle_chat): replace unfiltered stanza_get_delay()
  with trust-ordered fallback: our domain → sender domain → now.
- message.c (_handle_groupchat): replace stanza_get_oldest_delay()
  with fallback: room bare JID → room domain → our domain → now.
- message.c (_handle_muc_private_message): same pattern.

MAM path deliberately unchanged — <delay> inside <forwarded> is
already from our own server's archive.

XEP-0359 §5 (Unique and Stable Stanza IDs):
The <stanza-id> element was accepted without checking the 'by'
attribute, allowing a remote client to inject forged archive IDs.

- stanza.h: add STANZA_ATTR_BY define.
- message.c (_handle_groupchat): only accept <stanza-id> when
  by == room bare JID.
- message.c (_handle_chat): only accept <stanza-id> when
  by == our server domain.
This commit is contained in:
2026-02-21 18:24:53 +03:00
parent 31538580fb
commit 5f39dd4939
3 changed files with 64 additions and 20 deletions

View File

@@ -1089,12 +1089,16 @@ _handle_groupchat(xmpp_stanza_t* const stanza)
message->id = strdup(id); message->id = strdup(id);
} }
// XEP-0359 §5: only accept <stanza-id> whose 'by' matches the MUC room
char* stanzaid = NULL; char* stanzaid = NULL;
xmpp_stanza_t* stanzaidst = xmpp_stanza_get_child_by_name_and_ns(stanza, STANZA_NAME_STANZA_ID, STANZA_NS_STABLE_ID); xmpp_stanza_t* stanzaidst = xmpp_stanza_get_child_by_name_and_ns(stanza, STANZA_NAME_STANZA_ID, STANZA_NS_STABLE_ID);
if (stanzaidst) { if (stanzaidst) {
stanzaid = (char*)xmpp_stanza_get_attribute(stanzaidst, STANZA_ATTR_ID); const char* by = xmpp_stanza_get_attribute(stanzaidst, STANZA_ATTR_BY);
if (stanzaid) { if (by && g_strcmp0(by, from_jid->barejid) == 0) {
message->stanzaid = strdup(stanzaid); stanzaid = (char*)xmpp_stanza_get_attribute(stanzaidst, STANZA_ATTR_ID);
if (stanzaid) {
message->stanzaid = strdup(stanzaid);
}
} }
} }
@@ -1137,12 +1141,18 @@ _handle_groupchat(xmpp_stanza_t* const stanza)
message->timestamp = NULL; message->timestamp = NULL;
} }
// we want to display the oldest delay // XEP-0203 §4: only accept <delay> stamped by the room (MUC server)
message->timestamp = stanza_get_oldest_delay(stanza); // or our own server — reject client-forged timestamps.
message->timestamp = stanza_get_oldest_delay_from(stanza, from_jid->barejid);
// now this has nothing to do with MUC history if (!message->timestamp && from_jid->domainpart) {
// it's just setting the time to the received time so upon displaying we can use this time message->timestamp = stanza_get_oldest_delay_from(stanza, from_jid->domainpart);
// for example in win_println_incoming_muc_msg() }
if (!message->timestamp) {
const char* my_domain = connection_get_domain();
if (my_domain) {
message->timestamp = stanza_get_oldest_delay_from(stanza, my_domain);
}
}
if (!message->timestamp) { if (!message->timestamp) {
message->timestamp = g_date_time_new_now_local(); message->timestamp = g_date_time_new_now_local();
} }
@@ -1276,7 +1286,14 @@ _handle_muc_private_message(xmpp_stanza_t* const stanza)
} }
#endif #endif
message->timestamp = stanza_get_delay(stanza); // XEP-0203 §4: only trust <delay> from a server domain.
{
const char* my_domain = connection_get_domain();
message->timestamp = stanza_get_delay_from(stanza, (gchar*)my_domain);
if (!message->timestamp && message->from_jid->domainpart) {
message->timestamp = stanza_get_delay_from(stanza, message->from_jid->domainpart);
}
}
message->body = xmpp_message_get_body(stanza); message->body = xmpp_message_get_body(stanza);
if (!message->plain && !message->body) { if (!message->plain && !message->body) {
@@ -1400,13 +1417,17 @@ _handle_chat(xmpp_stanza_t* const stanza, gboolean is_mam, gboolean is_carbon, c
log_warning("MAM received with no result id"); log_warning("MAM received with no result id");
} }
} else { } else {
// live messages use XEP-0359 <stanza-id> // XEP-0359 §5: only accept <stanza-id> whose 'by' matches our server
char* stanzaid = NULL; char* stanzaid = NULL;
xmpp_stanza_t* stanzaidst = xmpp_stanza_get_child_by_name_and_ns(stanza, STANZA_NAME_STANZA_ID, STANZA_NS_STABLE_ID); xmpp_stanza_t* stanzaidst = xmpp_stanza_get_child_by_name_and_ns(stanza, STANZA_NAME_STANZA_ID, STANZA_NS_STABLE_ID);
if (stanzaidst) { if (stanzaidst) {
stanzaid = (char*)xmpp_stanza_get_attribute(stanzaidst, STANZA_ATTR_ID); const char* by = xmpp_stanza_get_attribute(stanzaidst, STANZA_ATTR_BY);
if (stanzaid) { const char* my_domain = connection_get_domain();
message->stanzaid = strdup(stanzaid); if (by && my_domain && g_strcmp0(by, my_domain) == 0) {
stanzaid = (char*)xmpp_stanza_get_attribute(stanzaidst, STANZA_ATTR_ID);
if (stanzaid) {
message->stanzaid = strdup(stanzaid);
}
} }
} }
} }
@@ -1424,8 +1445,13 @@ _handle_chat(xmpp_stanza_t* const stanza, gboolean is_mam, gboolean is_carbon, c
// timestamp provided outside like in a <forwarded> by MAM // timestamp provided outside like in a <forwarded> by MAM
message->timestamp = timestamp; message->timestamp = timestamp;
} else { } else {
// timestamp in the message stanza or use time of receival (now) // XEP-0203 §4: only trust <delay> from a server domain, not from
message->timestamp = stanza_get_delay(stanza); // the remote client which could forge an arbitrary timestamp.
const char* my_domain = connection_get_domain();
message->timestamp = stanza_get_delay_from(stanza, (gchar*)my_domain);
if (!message->timestamp && jid->domainpart) {
message->timestamp = stanza_get_delay_from(stanza, jid->domainpart);
}
if (!message->timestamp) { if (!message->timestamp) {
message->timestamp = g_date_time_new_now_local(); message->timestamp = g_date_time_new_now_local();
} }

View File

@@ -1215,6 +1215,12 @@ stanza_get_delay_from(xmpp_stanza_t* const stanza, gchar* from)
GDateTime* GDateTime*
stanza_get_oldest_delay(xmpp_stanza_t* const stanza) stanza_get_oldest_delay(xmpp_stanza_t* const stanza)
{
return stanza_get_oldest_delay_from(stanza, NULL);
}
GDateTime*
stanza_get_oldest_delay_from(xmpp_stanza_t* const stanza, const char* const from)
{ {
xmpp_stanza_t* child; xmpp_stanza_t* child;
const char* child_name; const char* child_name;
@@ -1225,27 +1231,37 @@ stanza_get_oldest_delay(xmpp_stanza_t* const stanza)
child_name = xmpp_stanza_get_name(child); child_name = xmpp_stanza_get_name(child);
if (child_name && g_strcmp0(child_name, STANZA_NAME_DELAY) == 0) { if (child_name && g_strcmp0(child_name, STANZA_NAME_DELAY) == 0) {
if (from) {
const char* child_from = xmpp_stanza_get_attribute(child, STANZA_ATTR_FROM);
if (!child_from || g_strcmp0(child_from, from) != 0)
continue;
}
GDateTime* tmp = _stanza_get_delay_timestamp_xep0203(child); GDateTime* tmp = _stanza_get_delay_timestamp_xep0203(child);
if (oldest == NULL) { if (oldest == NULL) {
oldest = tmp; oldest = tmp;
} else if (g_date_time_compare(oldest, tmp) == 1) { } else if (tmp && g_date_time_compare(oldest, tmp) == 1) {
g_date_time_unref(oldest); g_date_time_unref(oldest);
oldest = tmp; oldest = tmp;
} else { } else if (tmp) {
g_date_time_unref(tmp); g_date_time_unref(tmp);
} }
} }
if (child_name && g_strcmp0(child_name, STANZA_NAME_X) == 0) { if (child_name && g_strcmp0(child_name, STANZA_NAME_X) == 0) {
if (from) {
const char* child_from = xmpp_stanza_get_attribute(child, STANZA_ATTR_FROM);
if (!child_from || g_strcmp0(child_from, from) != 0)
continue;
}
GDateTime* tmp = _stanza_get_delay_timestamp_xep0091(child); GDateTime* tmp = _stanza_get_delay_timestamp_xep0091(child);
if (oldest == NULL) { if (oldest == NULL) {
oldest = tmp; oldest = tmp;
} else if (g_date_time_compare(oldest, tmp) == 1) { } else if (tmp && g_date_time_compare(oldest, tmp) == 1) {
g_date_time_unref(oldest); g_date_time_unref(oldest);
oldest = tmp; oldest = tmp;
} else { } else if (tmp) {
g_date_time_unref(tmp); g_date_time_unref(tmp);
} }
} }

View File

@@ -196,6 +196,7 @@
#define STANZA_ATTR_FILENAME "filename" #define STANZA_ATTR_FILENAME "filename"
#define STANZA_ATTR_SIZE "size" #define STANZA_ATTR_SIZE "size"
#define STANZA_ATTR_CONTENTTYPE "content-type" #define STANZA_ATTR_CONTENTTYPE "content-type"
#define STANZA_ATTR_BY "by"
#define STANZA_ATTR_LABEL "label" #define STANZA_ATTR_LABEL "label"
#define STANZA_TEXT_AWAY "away" #define STANZA_TEXT_AWAY "away"
@@ -331,6 +332,7 @@ gboolean stanza_contains_chat_state(xmpp_stanza_t* stanza);
GDateTime* stanza_get_delay(xmpp_stanza_t* const stanza); GDateTime* stanza_get_delay(xmpp_stanza_t* const stanza);
GDateTime* stanza_get_delay_from(xmpp_stanza_t* const stanza, gchar* from); GDateTime* stanza_get_delay_from(xmpp_stanza_t* const stanza, gchar* from);
GDateTime* stanza_get_oldest_delay(xmpp_stanza_t* const stanza); GDateTime* stanza_get_oldest_delay(xmpp_stanza_t* const stanza);
GDateTime* stanza_get_oldest_delay_from(xmpp_stanza_t* const stanza, const char* const from);
gboolean stanza_is_muc_presence(xmpp_stanza_t* const stanza); gboolean stanza_is_muc_presence(xmpp_stanza_t* const stanza);
gboolean stanza_is_muc_self_presence(xmpp_stanza_t* const stanza, gboolean stanza_is_muc_self_presence(xmpp_stanza_t* const stanza,