otr: cap reassembled multi-fragment message size (remote DoS)

enqueue_otr_fragment() reassembles ?OTR: multi-fragment OTR messages
into a per-peer heap buffer (opc->full_msg), grown by realloc on every
fragment that does not end with the OTR end tag '.'. The buffer has no
total size limit.

A remote IRC user can open a reassembly by sending a query PRIVMSG whose
body begins with ?OTR: and lacks the trailing '.', then keep sending
further PRIVMSGs: each is appended to opc->full_msg (roughly one
IRC-line-length worth of bytes per fragment) with no bound, growing it
until the victim irssi is OOM-killed. No OTR session is required -- the
peer context is created lazily by otr_find_context(..., create=1) on
first contact in otr_receive(), and sig_message_private
(signal_add_first on "message private") feeds every query PRIVMSG
straight to otr_receive()/enqueue_otr_fragment().

Add an OTR_REASM_MAX_SIZE (256 KiB) cap on the total reassembled size.
When appending a fragment would exceed it, free and reset the
reassembly state and return OTR_MSG_ERROR, on both the
subsequent-fragment and initial-fragment paths. 256 KiB is far above
any legitimate OTR message, so there is no functional regression.
This commit is contained in:
Acts1631 2026-07-06 10:41:55 -04:00
commit a5bd495733
2 changed files with 24 additions and 0 deletions

View file

@ -615,6 +615,18 @@ static enum otr_msg_status enqueue_otr_fragment(const char *msg, struct otr_peer
}
if (opc->full_msg) {
/* Reject the reassembly if appending this fragment would exceed
* the maximum assembled size. Without this check a peer can keep
* the reassembly open forever (fragments never end with '.') and
* grow opc->full_msg without bound, exhausting memory. */
if (opc->msg_len + msg_len + 1 > OTR_REASM_MAX_SIZE) {
free(opc->full_msg);
opc->full_msg = NULL;
opc->msg_size = opc->msg_len = 0;
ret = OTR_MSG_ERROR;
return ret;
}
/* Grow the buffer unless it already has room for msg_len bytes
* *plus* the NUL terminator written below. Using '>' here (instead
* of '>=') is an off-by-one: when msg_len exactly equals the
@ -674,6 +686,12 @@ static enum otr_msg_status enqueue_otr_fragment(const char *msg, struct otr_peer
*/
pos = strstr(msg, OTR_MSG_BEGIN_TAG);
if (pos && (pos == msg) && msg[msg_len - 1] != OTR_MSG_END_TAG) {
/* Reject oversized initial fragment. */
if ((msg_len * 2) + 1 > OTR_REASM_MAX_SIZE) {
ret = OTR_MSG_ERROR;
return ret;
}
/* Allocate full message buffer with an extra for NULL byte. */
opc->full_msg = g_new0(char, (msg_len * 2) + 1);
if (!opc->full_msg) {

View file

@ -54,6 +54,12 @@
#define OTR_MSG_BEGIN_TAG "?OTR:"
#define OTR_MSG_END_TAG '.'
/* Maximum total size of a reassembled multi-fragment OTR message. Incoming
* ?OTR: fragments are concatenated into a per-peer heap buffer until the
* end tag is received; this caps that buffer so a peer cannot grow it
* without bound by sending fragments that never terminate. */
#define OTR_REASM_MAX_SIZE (256 * 1024)
/* IRC /me command marker and len. */
#define OTR_IRC_MARKER_ME "/me "
#define OTR_IRC_MARKER_ME_LEN sizeof(OTR_IRC_MARKER_ME) - 1