Version in base suite: 13.0.1-1 Base version: prosody_13.0.1-1 Target version: prosody_13.0.1-1+deb131u Base file: /srv/ftp-master.debian.org/ftp/pool/main/p/prosody/prosody_13.0.1-1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/p/prosody/prosody_13.0.1-1+deb131u.dsc changelog | 9 patches/0005-mod_proxy65-Consistently-apply-authorization-checks.patch | 68 ++++ patches/0006-mod_proxy65-Don-t-link-connections-until-after-activ.patch | 35 ++ patches/0007-mod_c2s-Remove-timers-immediately-on-disconnection.patch | 53 +++ patches/0008-net.server_epoll-Clean-up-timers-after-disconnection.patch | 45 +++ patches/0009-mod_cloud_notify-Fix-leaking-iq-response-handlers-by.patch | 89 ++++++ patches/0010-moduleapi-Use-multitable-add-remove-instead-of-set.patch | 38 ++ patches/0011-mod_c2s-mod_s2s-Introduce-separate-pre-authenticatio.patch | 137 ++++++++++ patches/0012-util.xmppstream-Reduce-default-stanza-size-limit-to-.patch | 22 + patches/0013-mod_bosh-Apply-different-stanza-size-limit-before-au.patch | 63 ++++ patches/0014-util.xmppstream-Support-limiting-total-number-of-des.patch | 101 +++++++ patches/0015-mod_c2s-mod_s2s-Add-configurable-limit-for-stanza-ma.patch | 56 ++++ patches/series | 11 13 files changed, 727 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmppya59zqv/prosody_13.0.1-1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmppya59zqv/prosody_13.0.1-1+deb131u.dsc: no acceptable signature found diff -Nru prosody-13.0.1/debian/changelog prosody-13.0.1/debian/changelog --- prosody-13.0.1/debian/changelog 2025-04-04 00:10:46.000000000 +0000 +++ prosody-13.0.1/debian/changelog 2026-05-05 07:37:27.000000000 +0000 @@ -1,3 +1,12 @@ +prosody (13.0.1-1+deb131u) trixie-security; urgency=medium + + * CVE-2026-43504 fix + * CVE-2026-43505 fix + * CVE-2026-43506 fix + * CVE-2026-43507 fix + + -- Victor Seva Tue, 05 May 2026 09:37:27 +0200 + prosody (13.0.1-1) unstable; urgency=medium * Team upload diff -Nru prosody-13.0.1/debian/patches/0005-mod_proxy65-Consistently-apply-authorization-checks.patch prosody-13.0.1/debian/patches/0005-mod_proxy65-Consistently-apply-authorization-checks.patch --- prosody-13.0.1/debian/patches/0005-mod_proxy65-Consistently-apply-authorization-checks.patch 1970-01-01 00:00:00.000000000 +0000 +++ prosody-13.0.1/debian/patches/0005-mod_proxy65-Consistently-apply-authorization-checks.patch 2026-05-05 07:37:27.000000000 +0000 @@ -0,0 +1,68 @@ +From: Matthew Wild +Date: Tue, 5 May 2026 09:19:01 +0200 +Subject: mod_proxy65: Consistently apply authorization checks + +The module checked for authorization when a client asked for the address:port +of the proxy service. It did not check for authorization when processing a +request to activate a bytestream. This meant that any unauthenticated party +able to guess the IP/port and XMPP domain of a proxy65 service (generally low +difficulty) would be able to use the proxy to relay traffic between two +connections. + +This factors out the permission check, and applies it to every request type. +--- + plugins/mod_proxy65.lua | 25 +++++++++++++++---------- + 1 file changed, 15 insertions(+), 10 deletions(-) + +diff --git a/plugins/mod_proxy65.lua b/plugins/mod_proxy65.lua +index 38acc79..f12aa0c 100644 +--- a/plugins/mod_proxy65.lua ++++ b/plugins/mod_proxy65.lua +@@ -105,25 +105,24 @@ function module.add_host(module) + module:add_identity("proxy", "bytestreams", name); + module:add_feature("http://jabber.org/protocol/bytestreams"); + +- module:hook("iq-get/host/http://jabber.org/protocol/bytestreams:query", function(event) +- local origin, stanza = event.origin, event.stanza; +- +- -- check ACL +- -- using 'while' instead of 'if' so we can break out of it +- local allow; ++ local function is_permitted(origin, stanza) + if proxy_acl and #proxy_acl > 0 then + local jid = stanza.attr.from; + for _, acl in ipairs(proxy_acl) do + if jid_compare(jid, acl) then +- allow = true; +- break; ++ return true; + end + end + elseif proxy_open_access or origin.type == "c2s" then +- allow = true; ++ return true; + end ++ return false; ++ end + +- if not allow then ++ module:hook("iq-get/host/http://jabber.org/protocol/bytestreams:query", function(event) ++ local origin, stanza = event.origin, event.stanza; ++ ++ if not is_permitted(origin, stanza) then + module:log("warn", "Denying use of proxy for %s", stanza.attr.from); + origin.send(st.error_reply(stanza, "auth", "forbidden")); + return true; +@@ -145,6 +144,12 @@ function module.add_host(module) + module:hook("iq-set/host/http://jabber.org/protocol/bytestreams:query", function(event) + local origin, stanza = event.origin, event.stanza; + ++ if not is_permitted(origin, stanza) then ++ module:log("warn", "Denying use of proxy for %s", stanza.attr.from); ++ origin.send(st.error_reply(stanza, "auth", "forbidden")); ++ return true; ++ end ++ + local query = stanza.tags[1]; + local sid = query.attr.sid; + local from = stanza.attr.from; diff -Nru prosody-13.0.1/debian/patches/0006-mod_proxy65-Don-t-link-connections-until-after-activ.patch prosody-13.0.1/debian/patches/0006-mod_proxy65-Don-t-link-connections-until-after-activ.patch --- prosody-13.0.1/debian/patches/0006-mod_proxy65-Don-t-link-connections-until-after-activ.patch 1970-01-01 00:00:00.000000000 +0000 +++ prosody-13.0.1/debian/patches/0006-mod_proxy65-Don-t-link-connections-until-after-activ.patch 2026-05-05 07:37:27.000000000 +0000 @@ -0,0 +1,35 @@ +From: Matthew Wild +Date: Tue, 5 May 2026 09:23:07 +0200 +Subject: mod_proxy65: Don't link connections until after activation + +Although the module pauses the connections, in some cases server.link() (at +least with the epoll backend) will have the effect of unpausing the connection +(because it sets new read handlers). + +Many thanks to Max Hearnden for discovering and reporting this issue. +--- + plugins/mod_proxy65.lua | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/plugins/mod_proxy65.lua b/plugins/mod_proxy65.lua +index f12aa0c..8c03263 100644 +--- a/plugins/mod_proxy65.lua ++++ b/plugins/mod_proxy65.lua +@@ -60,8 +60,6 @@ function listener.onincoming(conn, data) + transfers[sha].initiator = conn; + session.sha = sha; + module:log("debug", "SOCKS5 initiator connected for session %s", sha); +- server.link(conn, transfers[sha].target, max_buffer_size); +- server.link(transfers[sha].target, conn, max_buffer_size); + end + else -- error, unexpected input + conn:write("\5\1\0\3\0\0\0"); -- VER, REP, RSV, ATYP, BND.ADDR (sha), BND.PORT (2 Byte) +@@ -171,6 +169,8 @@ function module.add_host(module) + else -- if transfers[sha].initiator ~= nil and transfers[sha].target ~= nil then + module:log("debug", "Transfer activated (%s)", info); + transfers[sha].activated = true; ++ server.link(transfers[sha].initiator, transfers[sha].target, max_buffer_size); ++ server.link(transfers[sha].target, transfers[sha].initiator, max_buffer_size); + transfers[sha].target:resume(); + transfers[sha].initiator:resume(); + origin.send(st.reply(stanza)); diff -Nru prosody-13.0.1/debian/patches/0007-mod_c2s-Remove-timers-immediately-on-disconnection.patch prosody-13.0.1/debian/patches/0007-mod_c2s-Remove-timers-immediately-on-disconnection.patch --- prosody-13.0.1/debian/patches/0007-mod_c2s-Remove-timers-immediately-on-disconnection.patch 1970-01-01 00:00:00.000000000 +0000 +++ prosody-13.0.1/debian/patches/0007-mod_c2s-Remove-timers-immediately-on-disconnection.patch 2026-05-05 07:37:27.000000000 +0000 @@ -0,0 +1,53 @@ +From: Waqas Hussain +Date: Tue, 5 May 2026 09:26:04 +0200 +Subject: mod_c2s: Remove timers immediately on disconnection + +--- + plugins/mod_c2s.lua | 13 ++++++++++++- + 1 file changed, 12 insertions(+), 1 deletion(-) + +diff --git a/plugins/mod_c2s.lua b/plugins/mod_c2s.lua +index c1fbdb9..6f8fe37 100644 +--- a/plugins/mod_c2s.lua ++++ b/plugins/mod_c2s.lua +@@ -8,7 +8,9 @@ + + module:set_global(); + +-local add_task = require "prosody.util.timer".add_task; ++local timer = require "prosody.util.timer"; ++local add_task = timer.add_task; ++local stop_task = timer.stop; + local new_xmpp_stream = require "prosody.util.xmppstream".new; + local nameprep = require "prosody.util.encodings".stringprep.nameprep; + local sessionmanager = require "prosody.core.sessionmanager"; +@@ -25,6 +27,13 @@ local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams"; + + local log = module._log; + ++local function stop_session_timers(session) ++ if session.c2s_timeout then ++ stop_task(session.c2s_timeout); ++ session.c2s_timeout = nil; ++ end ++end ++ + local c2s_timeout = module:get_option_period("c2s_timeout", "5 minutes"); + local stream_close_timeout = module:get_option_period("c2s_close_timeout", 5); + local opt_keepalives = module:get_option_boolean("c2s_tcp_keepalives", module:get_option_boolean("tcp_keepalives", true)); +@@ -212,6 +221,7 @@ local function session_close(session, reason) + local close_event_payload = { session = session, reason = reason }; + module:context(session.host):fire_event("pre-session-close", close_event_payload); + reason = close_event_payload.reason; ++ stop_session_timers(session); + if session.conn then + if session.notopen then + session:open_stream(); +@@ -419,6 +429,7 @@ function listener.ondisconnect(conn, err) + local session = sessions[conn]; + if session then + (session.log or log)("info", "Client disconnected: %s", err or "connection closed"); ++ stop_session_timers(session); + sm_destroy_session(session, err); + session.conn = nil; + sessions[conn] = nil; diff -Nru prosody-13.0.1/debian/patches/0008-net.server_epoll-Clean-up-timers-after-disconnection.patch prosody-13.0.1/debian/patches/0008-net.server_epoll-Clean-up-timers-after-disconnection.patch --- prosody-13.0.1/debian/patches/0008-net.server_epoll-Clean-up-timers-after-disconnection.patch 1970-01-01 00:00:00.000000000 +0000 +++ prosody-13.0.1/debian/patches/0008-net.server_epoll-Clean-up-timers-after-disconnection.patch 2026-05-05 07:37:27.000000000 +0000 @@ -0,0 +1,45 @@ +From: Waqas Hussain +Date: Tue, 5 May 2026 09:26:42 +0200 +Subject: net.server_epoll: Clean up timers after disconnection + +--- + net/server_epoll.lua | 12 +++++++++--- + 1 file changed, 9 insertions(+), 3 deletions(-) + +diff --git a/net/server_epoll.lua b/net/server_epoll.lua +index ca5a950..7b4edac 100644 +--- a/net/server_epoll.lua ++++ b/net/server_epoll.lua +@@ -154,9 +154,8 @@ local function noop() end + local closedtimers = {}; + + local function closetimer(id) +- if timers:remove(id) then +- closedtimers[id] = true; +- end ++ closedtimers[id] = true; ++ timers:remove(id); + end + + local function reschedule(id, time) +@@ -697,6 +696,10 @@ function interface:destroy() + -- make sure tls sockets aren't put in blocking mode + if self.conn.shutdown and self._tls then self.conn:shutdown(); end + self:del(); ++ if self._pausefor then ++ closetimer(self._pausefor); ++ self._pausefor = nil; ++ end + self:setwritetimeout(false); + self:setreadtimeout(false); + self.onreadable = noop; +@@ -961,6 +964,9 @@ function interface:pausefor(t) + self:set(false); + self._pausefor = addtimer(t, function () + self._pausefor = nil; ++ if not self.conn then ++ return; ++ end + self:set(true); + self:noise("Resuming after pause"); + if self.conn and self.conn:dirty() then diff -Nru prosody-13.0.1/debian/patches/0009-mod_cloud_notify-Fix-leaking-iq-response-handlers-by.patch prosody-13.0.1/debian/patches/0009-mod_cloud_notify-Fix-leaking-iq-response-handlers-by.patch --- prosody-13.0.1/debian/patches/0009-mod_cloud_notify-Fix-leaking-iq-response-handlers-by.patch 1970-01-01 00:00:00.000000000 +0000 +++ prosody-13.0.1/debian/patches/0009-mod_cloud_notify-Fix-leaking-iq-response-handlers-by.patch 2026-05-05 07:37:27.000000000 +0000 @@ -0,0 +1,89 @@ +From: Matthew Wild +Date: Tue, 5 May 2026 09:27:18 +0200 +Subject: mod_cloud_notify: Fix leaking iq response handlers by using + send_iq() + +--- + plugins/mod_cloud_notify.lua | 32 +++++++------------------------- + 1 file changed, 7 insertions(+), 25 deletions(-) + +diff --git a/plugins/mod_cloud_notify.lua b/plugins/mod_cloud_notify.lua +index 1c660e9..0280e5b 100644 +--- a/plugins/mod_cloud_notify.lua ++++ b/plugins/mod_cloud_notify.lua +@@ -74,8 +74,7 @@ end)(); + -- Forward declarations, as both functions need to reference each other + local handle_push_success, handle_push_error; + +-function handle_push_error(event) +- local stanza = event.stanza; ++function handle_push_error(stanza) + local error_type, condition, error_text = stanza:get_error(); + local node = id2node[stanza.attr.id]; + local identifier = id2identifier[stanza.attr.id]; +@@ -117,11 +116,6 @@ function handle_push_error(event) + changed = true; + user_push_services[push_identifier] = nil + push_errors[push_identifier] = nil; +- -- unhook iq handlers for this identifier (if possible) +- module:unhook("iq-error/host/"..stanza.attr.id, handle_push_error); +- module:unhook("iq-result/host/"..stanza.attr.id, handle_push_success); +- id2node[stanza.attr.id] = nil; +- id2identifier[stanza.attr.id] = nil; + end + elseif user_push_services[push_identifier] and user_push_services[push_identifier].jid == from and error_type == "wait" then + module:log("debug", "Got error <%s:%s:%s> for identifier '%s': " +@@ -141,8 +135,7 @@ function handle_push_error(event) + return true; + end + +-function handle_push_success(event) +- local stanza = event.stanza; ++function handle_push_success(stanza) + local node = id2node[stanza.attr.id]; + local identifier = id2identifier[stanza.attr.id]; + if node == nil then return false; end -- unknown stanza? Ignore for now! +@@ -153,11 +146,6 @@ function handle_push_success(event) + if push_identifier == identifier then + if user_push_services[push_identifier] and user_push_services[push_identifier].jid == from and push_errors[push_identifier] > 0 then + push_errors[push_identifier] = 0; +- -- unhook iq handlers for this identifier (if possible) +- module:unhook("iq-error/host/"..stanza.attr.id, handle_push_error); +- module:unhook("iq-result/host/"..stanza.attr.id, handle_push_success); +- id2node[stanza.attr.id] = nil; +- id2identifier[stanza.attr.id] = nil; + module:log("debug", "Push succeeded, error count for identifier '%s' is now at %s again", + push_identifier, tostring(push_errors[push_identifier]) + ); +@@ -257,14 +245,6 @@ local function push_disable(event) + end + user_push_services[key] = nil; + push_errors[key] = nil; +- for stanza_id, identifier in pairs(id2identifier) do +- if identifier == key then +- module:unhook("iq-error/host/"..stanza_id, handle_push_error); +- module:unhook("iq-result/host/"..stanza_id, handle_push_success); +- id2node[stanza_id] = nil; +- id2identifier[stanza_id] = nil; +- end +- end + end + end + local ok = push_store:flush_to_disk(origin.username); +@@ -413,11 +393,13 @@ local function handle_notify_request(stanza, node, user_push_services, log_push_ + if push_errors[push_identifier] == nil then + push_errors[push_identifier] = 0; + end +- module:hook("iq-error/host/"..stanza_id, handle_push_error); +- module:hook("iq-result/host/"..stanza_id, handle_push_success); + id2node[stanza_id] = node; + id2identifier[stanza_id] = push_identifier; +- module:send(push_publish); ++ module:send_iq(push_publish):next(handle_push_success, handle_push_error):finally(function () ++ -- unhook iq handlers for this identifier (if possible) ++ id2node[stanza.attr.id] = nil; ++ id2identifier[stanza.attr.id] = nil; ++ end); + pushes = pushes + 1; + end + end diff -Nru prosody-13.0.1/debian/patches/0010-moduleapi-Use-multitable-add-remove-instead-of-set.patch prosody-13.0.1/debian/patches/0010-moduleapi-Use-multitable-add-remove-instead-of-set.patch --- prosody-13.0.1/debian/patches/0010-moduleapi-Use-multitable-add-remove-instead-of-set.patch 1970-01-01 00:00:00.000000000 +0000 +++ prosody-13.0.1/debian/patches/0010-moduleapi-Use-multitable-add-remove-instead-of-set.patch 2026-05-05 07:37:27.000000000 +0000 @@ -0,0 +1,38 @@ +From: Matthew Wild +Date: Tue, 5 May 2026 09:27:57 +0200 +Subject: moduleapi: Use multitable add/remove instead of set + +multitable:set() does not clear intermediate keys when setting the last one to +nil. This meant that the event_handlers multitable for every module was not +properly cleaning up the object and event name from the table when calling +:unhook_object_event(). + +This combined badly with e.g. mod_bookmarks, which uses this extensively to +add/remove hooks dynamically for mod_pep services. The multitable kept a +reference to every mod_pep service object ever created, causing a memory leak. + +The multitable:remove() function clears intermediate keys when they are empty, +so we've switched to using that now. +--- + core/moduleapi.lua | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/core/moduleapi.lua b/core/moduleapi.lua +index 50524b3..ef4acbf 100644 +--- a/core/moduleapi.lua ++++ b/core/moduleapi.lua +@@ -83,12 +83,12 @@ function api:fire_event(...) + end + + function api:hook_object_event(object, event, handler, priority) +- self.event_handlers:set(object, event, handler, true); ++ self.event_handlers:add(object, event, handler, true); + return object.add_handler(event, handler, priority); + end + + function api:unhook_object_event(object, event, handler) +- self.event_handlers:set(object, event, handler, nil); ++ self.event_handlers:remove(object, event, handler); + return object.remove_handler(event, handler); + end + diff -Nru prosody-13.0.1/debian/patches/0011-mod_c2s-mod_s2s-Introduce-separate-pre-authenticatio.patch prosody-13.0.1/debian/patches/0011-mod_c2s-mod_s2s-Introduce-separate-pre-authenticatio.patch --- prosody-13.0.1/debian/patches/0011-mod_c2s-mod_s2s-Introduce-separate-pre-authenticatio.patch 1970-01-01 00:00:00.000000000 +0000 +++ prosody-13.0.1/debian/patches/0011-mod_c2s-mod_s2s-Introduce-separate-pre-authenticatio.patch 2026-05-05 07:37:27.000000000 +0000 @@ -0,0 +1,137 @@ +From: Kim Alvefur +Date: Tue, 5 May 2026 09:31:50 +0200 +Subject: mod_c2s,mod_s2s: Introduce separate pre-authentication stanza size + limit + +This should prevent unauthenticated resource use via the XML parser. +--- + plugins/mod_c2s.lua | 20 +++++++++++++++----- + plugins/mod_s2s.lua | 23 +++++++++++++++-------- + 2 files changed, 30 insertions(+), 13 deletions(-) + +diff --git a/plugins/mod_c2s.lua b/plugins/mod_c2s.lua +index 6f8fe37..11cf236 100644 +--- a/plugins/mod_c2s.lua ++++ b/plugins/mod_c2s.lua +@@ -37,7 +37,8 @@ end + local c2s_timeout = module:get_option_period("c2s_timeout", "5 minutes"); + local stream_close_timeout = module:get_option_period("c2s_close_timeout", 5); + local opt_keepalives = module:get_option_boolean("c2s_tcp_keepalives", module:get_option_boolean("tcp_keepalives", true)); +-local stanza_size_limit = module:get_option_integer("c2s_stanza_size_limit", 1024*256,10000); ++local unauthed_stanza_size_limit = module:get_option_integer("c2s_unauthed_stanza_size_limit", 10000,1000); ++local authed_stanza_size_limit = module:get_option_integer("c2s_stanza_size_limit", 1024*256,10000); + + local advertised_idle_timeout = 14*60; -- default in all net.server implementations + local network_settings = module:get_option("network_settings"); +@@ -146,11 +147,11 @@ function session_events.streamopened(session, event) + local features = st.stanza("stream:features"); + hosts[session.host].events.fire_event("stream-features", { origin = session, features = features, stream = attr }); + if features.tags[1] or session.full_jid then +- if stanza_size_limit or advertised_idle_timeout then ++ if session.stanza_size_limit or advertised_idle_timeout then + features:reset(); + local limits = features:tag("limits", { xmlns = "urn:xmpp:stream-limits:0" }); +- if stanza_size_limit then +- limits:text_tag("max-bytes", string.format("%d", stanza_size_limit)); ++ if session.stanza_size_limit then ++ limits:text_tag("max-bytes", string.format("%d", session.stanza_size_limit)); + end + if advertised_idle_timeout then + limits:text_tag("idle-seconds", string.format("%d", advertised_idle_timeout)); +@@ -362,7 +363,9 @@ function listener.onconnect(conn) + + session.close = session_close; + +- local stream = new_xmpp_stream(session, stream_callbacks, stanza_size_limit); ++ session.stanza_size_limit = unauthed_stanza_size_limit; ++ ++ local stream = new_xmpp_stream(session, stream_callbacks, session.stanza_size_limit); + session.stream = stream; + session.notopen = true; + +@@ -471,6 +474,13 @@ end + + function module.add_host(module) + module:hook("c2s-read-timeout", keepalive, -1); ++ module:hook("authentication-success", function(event) ++ local session = event.session; ++ if session.stream then ++ session.stanza_size_limit = authed_stanza_size_limit; ++ session.stream:set_stanza_size_limit(session.stanza_size_limit); ++ end ++ end); + end + + module:hook("c2s-read-timeout", keepalive, -1); +diff --git a/plugins/mod_s2s.lua b/plugins/mod_s2s.lua +index 5b81cf4..d6007cc 100644 +--- a/plugins/mod_s2s.lua ++++ b/plugins/mod_s2s.lua +@@ -41,7 +41,8 @@ local secure_auth = module:get_option_boolean("s2s_secure_auth", false); -- One + local secure_domains, insecure_domains = + module:get_option_set("s2s_secure_domains", {})._items, module:get_option_set("s2s_insecure_domains", {})._items; + local require_encryption = module:get_option_boolean("s2s_require_encryption", true); +-local stanza_size_limit = module:get_option_integer("s2s_stanza_size_limit", 1024*512, 10000); ++local unauthed_stanza_size_limit = module:get_option_integer("s2s_unauthed_stanza_size_limit", 10000, 1000); ++local authed_stanza_size_limit = module:get_option_integer("s2s_stanza_size_limit", 1024*512, 10000); + local sendq_size = module:get_option_integer("s2s_send_queue_size", 1024*32, 1); + + local advertised_idle_timeout = 14*60; -- default in all net.server implementations +@@ -225,6 +226,8 @@ function route_to_new_session(event) + local host_session = s2s_new_outgoing(from_host, to_host); + host_session.version = 1; + ++ host_session.stanza_size_limit = unauthed_stanza_size_limit; ++ + -- Store in buffer + host_session.bounce_sendq = bounce_sendq; + host_session.sendq = queue.new(sendq_size); +@@ -269,10 +272,11 @@ function module.add_host(module) + module:hook("route/remote", route_to_existing_session, -1); + module:hook("route/remote", route_to_new_session, -10); + module:hook("s2sout-stream-features", function (event) +- if not (stanza_size_limit or advertised_idle_timeout) then return end ++ local session = event.origin; ++ if not (session.stanza_size_limit or advertised_idle_timeout) then return end + local limits = event.features:tag("limits", { xmlns = "urn:xmpp:stream-limits:0" }) +- if stanza_size_limit then +- limits:text_tag("max-bytes", string.format("%d", stanza_size_limit)); ++ if session.stanza_size_limit then ++ limits:text_tag("max-bytes", string.format("%d", session.stanza_size_limit)); + end + if advertised_idle_timeout then + limits:text_tag("idle-seconds", string.format("%d", advertised_idle_timeout)); +@@ -414,6 +418,9 @@ function make_authenticated(event) + return false; + end + ++ session.stanza_size_limit = authed_stanza_size_limit; ++ session.stream:set_stanza_size_limit(session.stanza_size_limit); ++ + if session.incoming and host then + if not session.hosts[host] then session.hosts[host] = {}; end + session.hosts[host].authed = true; +@@ -568,11 +575,11 @@ function session_events.streamopened(session, event) + end + + if ( session.type == "s2sin" or session.type == "s2sout" ) or features.tags[1] then +- if stanza_size_limit or advertised_idle_timeout then ++ if session.stanza_size_limit or advertised_idle_timeout then + features:reset(); + local limits = features:tag("limits", { xmlns = "urn:xmpp:stream-limits:0" }); +- if stanza_size_limit then +- limits:text_tag("max-bytes", string.format("%d", stanza_size_limit)); ++ if session.stanza_size_limit then ++ limits:text_tag("max-bytes", string.format("%d", session.stanza_size_limit)); + end + if advertised_idle_timeout then + limits:text_tag("idle-seconds", string.format("%d", advertised_idle_timeout)); +@@ -789,7 +796,7 @@ end + + -- Session initialization logic shared by incoming and outgoing + local function initialize_session(session) +- local stream = new_xmpp_stream(session, stream_callbacks, stanza_size_limit); ++ local stream = new_xmpp_stream(session, stream_callbacks, session.stanza_size_limit or unauthed_stanza_size_limit); + + session.thread = runner(function (item) + if st.is_stanza(item) then diff -Nru prosody-13.0.1/debian/patches/0012-util.xmppstream-Reduce-default-stanza-size-limit-to-.patch prosody-13.0.1/debian/patches/0012-util.xmppstream-Reduce-default-stanza-size-limit-to-.patch --- prosody-13.0.1/debian/patches/0012-util.xmppstream-Reduce-default-stanza-size-limit-to-.patch 1970-01-01 00:00:00.000000000 +0000 +++ prosody-13.0.1/debian/patches/0012-util.xmppstream-Reduce-default-stanza-size-limit-to-.patch 2026-05-05 07:37:27.000000000 +0000 @@ -0,0 +1,22 @@ +From: Kim Alvefur +Date: Tue, 5 May 2026 09:32:20 +0200 +Subject: util.xmppstream: Reduce default stanza size limit to 10k for safety + +To provide a safe default in case the limit is forgotten. +--- + util/xmppstream.lua | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/util/xmppstream.lua b/util/xmppstream.lua +index 7f99831..83ec80c 100644 +--- a/util/xmppstream.lua ++++ b/util/xmppstream.lua +@@ -22,7 +22,7 @@ local lxp_supports_doctype = pcall(lxp.new, { StartDoctypeDecl = false }); + local lxp_supports_xmldecl = pcall(lxp.new, { XmlDecl = false }); + local lxp_supports_bytecount = not not lxp.new({}).getcurrentbytecount; + +-local default_stanza_size_limit = 1024*1024*1; -- 1MB ++local default_stanza_size_limit = 10000; -- Minimum stanza size limit + + local _ENV = nil; + -- luacheck: std none diff -Nru prosody-13.0.1/debian/patches/0013-mod_bosh-Apply-different-stanza-size-limit-before-au.patch prosody-13.0.1/debian/patches/0013-mod_bosh-Apply-different-stanza-size-limit-before-au.patch --- prosody-13.0.1/debian/patches/0013-mod_bosh-Apply-different-stanza-size-limit-before-au.patch 1970-01-01 00:00:00.000000000 +0000 +++ prosody-13.0.1/debian/patches/0013-mod_bosh-Apply-different-stanza-size-limit-before-au.patch 2026-05-05 07:37:27.000000000 +0000 @@ -0,0 +1,63 @@ +From: Kim Alvefur +Date: Tue, 5 May 2026 09:32:46 +0200 +Subject: mod_bosh: Apply different stanza size limit before authentication + +This is a bit tricky since each request creates a new stream and we +have to start parsing the stream before we know if it belongs to an +already authenticated session. +--- + plugins/mod_bosh.lua | 12 ++++++++++-- + 1 file changed, 10 insertions(+), 2 deletions(-) + +diff --git a/plugins/mod_bosh.lua b/plugins/mod_bosh.lua +index 2d1b192..937e2a5 100644 +--- a/plugins/mod_bosh.lua ++++ b/plugins/mod_bosh.lua +@@ -45,7 +45,7 @@ local bosh_max_wait = module:get_option_period("bosh_max_wait", 120); + + local consider_bosh_secure = module:get_option_boolean("consider_bosh_secure"); + local cross_domain = module:get_option("cross_domain_bosh"); +-local stanza_size_limit = module:get_option_integer("c2s_stanza_size_limit", 1024*256, 10000); ++local unauthed_stanza_size_limit = module:get_option_integer("c2s_stanza_size_limit", 10000, 1000); + + if cross_domain ~= nil then + module:log("info", "The 'cross_domain_bosh' option has been deprecated"); +@@ -123,8 +123,9 @@ function handle_POST(event) + local body = request.body; + + local context = { request = request, response = response, notopen = true }; +- local stream = new_xmpp_stream(context, stream_callbacks, stanza_size_limit); ++ local stream = new_xmpp_stream(context, stream_callbacks, unauthed_stanza_size_limit); + response.context = context; ++ context.stream = stream; + + local headers = response.headers; + headers.content_type = "text/xml; charset=utf-8"; +@@ -333,6 +334,7 @@ function stream_callbacks.streamopened(context, attr) + close = bosh_close_stream, dispatch_stanza = core_process_stanza, notopen = true, + log = logger.init("bosh"..sid), secure = consider_bosh_secure or request.secure, + ip = request.ip; ++ stanza_size_limit = unauthed_stanza_size_limit; + }; + sessions[sid] = session; + +@@ -406,6 +408,11 @@ function stream_callbacks.streamopened(context, attr) + end + + session.conn = request.conn; ++ session.stream = context.stream; ++ ++ if session.stanza_size_limit then ++ session.stream:set_stanza_size_limit(session.stanza_size_limit); ++ end + + if session.rid then + local diff = rid - session.rid; +@@ -544,6 +551,7 @@ local function GET_response(event) + end + + function module.add_host(module) ++ module:depends("c2s"); -- updates stanza_size_limit on authentication + module:depends("http"); + module:provides("http", { + default_path = "/http-bind"; diff -Nru prosody-13.0.1/debian/patches/0014-util.xmppstream-Support-limiting-total-number-of-des.patch prosody-13.0.1/debian/patches/0014-util.xmppstream-Support-limiting-total-number-of-des.patch --- prosody-13.0.1/debian/patches/0014-util.xmppstream-Support-limiting-total-number-of-des.patch 1970-01-01 00:00:00.000000000 +0000 +++ prosody-13.0.1/debian/patches/0014-util.xmppstream-Support-limiting-total-number-of-des.patch 2026-05-05 07:37:27.000000000 +0000 @@ -0,0 +1,101 @@ +From: Matthew Wild +Date: Tue, 5 May 2026 09:33:32 +0200 +Subject: util.xmppstream: Support limiting total number of descendent + elements in a stanza + +--- + util/xmppstream.lua | 35 ++++++++++++++++++++++++++++++----- + 1 file changed, 30 insertions(+), 5 deletions(-) + +diff --git a/util/xmppstream.lua b/util/xmppstream.lua +index 83ec80c..4f1e561 100644 +--- a/util/xmppstream.lua ++++ b/util/xmppstream.lua +@@ -43,7 +43,7 @@ local ns_pattern = "^([^"..ns_separator.."]*)"..ns_separator.."?(.*)$"; + + local function dummy_cb() end + +-local function new_sax_handlers(session, stream_callbacks, cb_handleprogress) ++local function new_sax_handlers(session, stream_callbacks, cb_handleprogress, max_elements) + local xml_handlers = {}; + + local cb_streamopened = stream_callbacks.streamopened; +@@ -69,6 +69,7 @@ local function new_sax_handlers(session, stream_callbacks, cb_handleprogress) + local stack = {}; + local chardata, stanza = {}; + local stanza_size = 0; ++ local child_quota = 0; + local non_streamns_depth = 0; + function xml_handlers:StartElement(tagname, attr) + if stanza and #chardata > 0 then +@@ -86,6 +87,18 @@ local function new_sax_handlers(session, stream_callbacks, cb_handleprogress) + non_streamns_depth = non_streamns_depth + 1; + end + ++ if stanza and child_quota then ++ -- Check stanza complexity limits ++ child_quota = child_quota - 1; ++ if child_quota <= 0 then ++ cb_error(session, "parse-error", "resource-constraint", "Stanza exceeds permitted children"); ++ if not self.stop or not self:stop() then ++ error("Failed to abort parsing"); ++ end ++ return; ++ end ++ end ++ + for i=1,#attr do + local k = attr[i]; + attr[i] = nil; +@@ -122,6 +135,7 @@ local function new_sax_handlers(session, stream_callbacks, cb_handleprogress) + end + + stanza = setmetatable({ name = name, attr = attr, tags = {} }, stanza_mt); ++ child_quota = max_elements; + else -- we are inside a stanza, so add a tag + if lxp_supports_bytecount then + stanza_size = stanza_size + self:getcurrentbytecount(); +@@ -237,10 +251,18 @@ local function new_sax_handlers(session, stream_callbacks, cb_handleprogress) + session = new_session; + end + +- return xml_handlers, { reset = reset, set_session = set_session }; ++ local function set_max_elements(new_max_elements) ++ max_elements = new_max_elements; ++ end ++ ++ return xml_handlers, { ++ reset = reset; ++ set_session = set_session; ++ set_max_elements = set_max_elements; ++ }; + end + +-local function new(session, stream_callbacks, stanza_size_limit) ++local function new(session, stream_callbacks, stanza_size_limit, ex) + -- Used to track parser progress (e.g. to enforce size limits) + local n_outstanding_bytes = 0; + local handle_progress; +@@ -253,8 +275,10 @@ local function new(session, stream_callbacks, stanza_size_limit) + error("Stanza size limits are not supported on this version of LuaExpat") + end + ++ local max_elements = ex and ex.max_elements; ++ + local handlers, meta = new_sax_handlers(session, stream_callbacks, handle_progress); +- local parser = new_parser(handlers, ns_separator, false); ++ local parser = new_parser(handlers, ns_separator, false, max_elements); + local parse = parser.parse; + + function session.open_stream(session, from, to) -- luacheck: ignore 432/session +@@ -298,8 +322,9 @@ local function new(session, stream_callbacks, stanza_size_limit) + return ok, err; + end, + set_session = meta.set_session; +- set_stanza_size_limit = function (_, new_stanza_size_limit) ++ set_stanza_size_limit = function (_, new_stanza_size_limit, new_max_elements) + stanza_size_limit = new_stanza_size_limit; ++ meta.set_max_elements(new_max_elements); + end; + }; + end diff -Nru prosody-13.0.1/debian/patches/0015-mod_c2s-mod_s2s-Add-configurable-limit-for-stanza-ma.patch prosody-13.0.1/debian/patches/0015-mod_c2s-mod_s2s-Add-configurable-limit-for-stanza-ma.patch --- prosody-13.0.1/debian/patches/0015-mod_c2s-mod_s2s-Add-configurable-limit-for-stanza-ma.patch 1970-01-01 00:00:00.000000000 +0000 +++ prosody-13.0.1/debian/patches/0015-mod_c2s-mod_s2s-Add-configurable-limit-for-stanza-ma.patch 2026-05-05 07:37:27.000000000 +0000 @@ -0,0 +1,56 @@ +From: Matthew Wild +Date: Tue, 5 May 2026 09:34:01 +0200 +Subject: mod_c2s, + mod_s2s: Add configurable limit for stanza max child elements + +--- + plugins/mod_c2s.lua | 5 ++++- + plugins/mod_s2s.lua | 5 ++++- + 2 files changed, 8 insertions(+), 2 deletions(-) + +diff --git a/plugins/mod_c2s.lua b/plugins/mod_c2s.lua +index 11cf236..6543f31 100644 +--- a/plugins/mod_c2s.lua ++++ b/plugins/mod_c2s.lua +@@ -39,6 +39,7 @@ local stream_close_timeout = module:get_option_period("c2s_close_timeout", 5); + local opt_keepalives = module:get_option_boolean("c2s_tcp_keepalives", module:get_option_boolean("tcp_keepalives", true)); + local unauthed_stanza_size_limit = module:get_option_integer("c2s_unauthed_stanza_size_limit", 10000,1000); + local authed_stanza_size_limit = module:get_option_integer("c2s_stanza_size_limit", 1024*256,10000); ++local max_child_elements = module:get_option_integer("c2s_max_child_elements", 25000, 0); + + local advertised_idle_timeout = 14*60; -- default in all net.server implementations + local network_settings = module:get_option("network_settings"); +@@ -365,7 +366,9 @@ function listener.onconnect(conn) + + session.stanza_size_limit = unauthed_stanza_size_limit; + +- local stream = new_xmpp_stream(session, stream_callbacks, session.stanza_size_limit); ++ local stream = new_xmpp_stream(session, stream_callbacks, session.stanza_size_limit, { ++ max_elements = max_child_elements; ++ }); + session.stream = stream; + session.notopen = true; + +diff --git a/plugins/mod_s2s.lua b/plugins/mod_s2s.lua +index d6007cc..d866e9d 100644 +--- a/plugins/mod_s2s.lua ++++ b/plugins/mod_s2s.lua +@@ -43,6 +43,7 @@ local secure_domains, insecure_domains = + local require_encryption = module:get_option_boolean("s2s_require_encryption", true); + local unauthed_stanza_size_limit = module:get_option_integer("s2s_unauthed_stanza_size_limit", 10000, 1000); + local authed_stanza_size_limit = module:get_option_integer("s2s_stanza_size_limit", 1024*512, 10000); ++local max_child_elements = module:get_option_integer("s2s_max_child_elements", 25000, 0); + local sendq_size = module:get_option_integer("s2s_send_queue_size", 1024*32, 1); + + local advertised_idle_timeout = 14*60; -- default in all net.server implementations +@@ -796,7 +797,9 @@ end + + -- Session initialization logic shared by incoming and outgoing + local function initialize_session(session) +- local stream = new_xmpp_stream(session, stream_callbacks, session.stanza_size_limit or unauthed_stanza_size_limit); ++ local stream = new_xmpp_stream(session, stream_callbacks, session.stanza_size_limit, { ++ max_elements = max_child_elements; ++ }); + + session.thread = runner(function (item) + if st.is_stanza(item) then diff -Nru prosody-13.0.1/debian/patches/series prosody-13.0.1/debian/patches/series --- prosody-13.0.1/debian/patches/series 2025-03-18 22:32:55.000000000 +0000 +++ prosody-13.0.1/debian/patches/series 2026-05-05 07:37:27.000000000 +0000 @@ -1,3 +1,14 @@ 0001-conf.patch 0002-upstream-conf.patch 0004-fix-package.path-of-ejabberd2prosody.patch +0005-mod_proxy65-Consistently-apply-authorization-checks.patch +0006-mod_proxy65-Don-t-link-connections-until-after-activ.patch +0007-mod_c2s-Remove-timers-immediately-on-disconnection.patch +0008-net.server_epoll-Clean-up-timers-after-disconnection.patch +0009-mod_cloud_notify-Fix-leaking-iq-response-handlers-by.patch +0010-moduleapi-Use-multitable-add-remove-instead-of-set.patch +0011-mod_c2s-mod_s2s-Introduce-separate-pre-authenticatio.patch +0012-util.xmppstream-Reduce-default-stanza-size-limit-to-.patch +0013-mod_bosh-Apply-different-stanza-size-limit-before-au.patch +0014-util.xmppstream-Support-limiting-total-number-of-des.patch +0015-mod_c2s-mod_s2s-Add-configurable-limit-for-stanza-ma.patch