From c72b966e0aeabcd5da41d5c25d787b56f05d50cb Mon Sep 17 00:00:00 2001 From: reverse Date: Wed, 19 Aug 2026 00:40:58 +0000 Subject: [PATCH] =?UTF-8?q?mode:=20cap=20mode=20changes=20per=20MODE=20com?= =?UTF-8?q?mand=20(modes=3D,=20default=2020)=20and=20advertise=20it=20as?= =?UTF-8?q?=20the=20MODES=3D=20ISUPPORT=20token=20=E2=80=94=20an=20uncappe?= =?UTF-8?q?d=20modestring=20like=20MODE=20#c=20+bbbb=E2=80=A6=20dispatched?= =?UTF-8?q?=20a=20handler=20per=20letter,=20each=20fanning=20out=20to=20th?= =?UTF-8?q?e=20whole=20channel=20and=20every=20S2S=20link=20(amplification?= =?UTF-8?q?=20DoS);=20matches=20InspIRCd's=20MODES=20limit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/coremods/core_mode.rs | 9 +++++++++ src/server.rs | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/coremods/core_mode.rs b/src/coremods/core_mode.rs index 4b30e9f..fb1a552 100644 --- a/src/coremods/core_mode.rs +++ b/src/coremods/core_mode.rs @@ -119,11 +119,20 @@ pub fn apply_mode(s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { let mut echoed: Vec = Vec::new(); // (sign, letter, displayed-param) per applied change — for hidemode filtering let mut changes: Vec<(char, char, Option)> = Vec::new(); + // Cap mode changes per command (advertised as MODES=, default 20 like InspIRCd): + // otherwise `MODE #c +bbbb…` in one line dispatches hundreds of handlers, each + // fanning out to the whole channel and every link — a cheap amplification flood. + let max_modes = s.conf_num("modes", 20usize).max(1); + let mut processed = 0usize; for c in modestring.chars() { if c == '+' || c == '-' { sign = c; continue; } + if processed >= max_modes { + break; + } + processed += 1; let adding = sign == '+'; let Some(handler) = chan_mode(c) else { s.numeric( diff --git a/src/server.rs b/src/server.rs index 158de95..e220738 100644 --- a/src/server.rs +++ b/src/server.rs @@ -771,12 +771,13 @@ impl Server { let chathist = crate::modules::chathistory::limit(self); let maxnick = self.conf_num("maxnick", 30usize); let maxchan = self.conf_num("maxchannel", 50usize); + let maxmodes = self.conf_num("modes", 20usize).max(1); // operprefix/ojoin add the server oper prefix `y` above owner; sigils are // config-overridable (see modules::customprefix) let include_oper = self.conf_bool("operprefix", false) || self.conf_bool("ojoin", false); let prefix = crate::modules::customprefix::isupport(include_oper); let mut tokens: Vec = format!( - "CHANTYPES=# PREFIX={prefix} CHANMODES=beIgXw,k,lfjFLHBJdK,ACDGMNOPQRSTUcimnprstuz EXTBAN=,aGbcgjmnrsy ACCOUNTEXTBAN=a BOT=B WATCH={maxwatch} MONITOR={maxmon} SILENCE={maxsil} CALLERID=g WHOX CHATHISTORY={chathist} MSGREFTYPES=timestamp,msgid UTF8ONLY CASEMAPPING=ascii NICKLEN={maxnick} CHANNELLEN={maxchan} NETWORK={}", + "CHANTYPES=# PREFIX={prefix} CHANMODES=beIgXw,k,lfjFLHBJdK,ACDGMNOPQRSTUcimnprstuz EXTBAN=,aGbcgjmnrsy ACCOUNTEXTBAN=a BOT=B WATCH={maxwatch} MONITOR={maxmon} SILENCE={maxsil} CALLERID=g WHOX CHATHISTORY={chathist} MSGREFTYPES=timestamp,msgid UTF8ONLY CASEMAPPING=ascii NICKLEN={maxnick} CHANNELLEN={maxchan} MODES={maxmodes} NETWORK={}", self.network ) .split(' ')