harden: fix reachable panics (parse_duration/parse_iso/dechunk char-boundary+overflow), s2s netburst key/limit loss, rpc set_nick/set_vhost/notice injection, webirc rehash reload, panic-state reset, ws line cap, remote nick collision, per-conn state leaks

This commit is contained in:
Jean Chevronnet 2026-08-15 15:02:36 +00:00
parent 3fa9737ccb
commit e935ee7002
12 changed files with 87 additions and 11 deletions

View file

@ -28,6 +28,11 @@ impl Module for BlockAmsg {
fn name(&self) -> &'static str {
"blockamsg"
}
fn on_user_quit(&mut self, s: &mut Server, uid: Uid, _reason: &str) {
if let Some(m) = s.ext.get_mut::<LastMsg>() {
m.0.remove(&uid);
}
}
fn on_pre_command(
&mut self,

View file

@ -62,6 +62,11 @@ impl Module for CloudflareChallenge {
fn name(&self) -> &'static str {
"cloudflare_challenge"
}
fn on_user_quit(&mut self, s: &mut Server, uid: Uid, _reason: &str) {
if let Some(p) = s.ext.get_mut::<Passed>() {
p.0.remove(&uid);
}
}
fn on_user_register(&mut self, srv: &mut Server, uid: Uid) -> ModResult {
if !enabled(srv) || srv.is_oper(uid) {

View file

@ -71,6 +71,11 @@ impl Module for ReCaptcha {
fn name(&self) -> &'static str {
"recaptcha"
}
fn on_user_quit(&mut self, s: &mut Server, uid: Uid, _reason: &str) {
if let Some(v) = s.ext.get_mut::<Verified>() {
v.0.remove(&uid);
}
}
fn on_user_register(&mut self, srv: &mut Server, uid: Uid) -> ModResult {
if !enabled(srv) || srv.is_oper(uid) {

View file

@ -12,6 +12,9 @@ pub fn handle(s: &mut Server, action: &str, params: &str) -> Result<String, RpcE
.ok_or_else(|| RpcError::invalid_params("missing 'target'"))?;
let text = json::get_str(params, "message")
.ok_or_else(|| RpcError::invalid_params("missing 'message'"))?;
// strip CR/LF so a crafted message/target can't inject extra IRC lines
let strip = |v: String| -> String { v.chars().filter(|c| *c != '\r' && *c != '\n').collect() };
let (target, text) = (strip(target), strip(text));
let src = s.name.clone();
if target == "*" || target == "$*" {
let uids: Vec<crate::Uid> = s.users.keys().copied().collect();

View file

@ -107,6 +107,9 @@ pub fn handle(s: &mut Server, action: &str, params: &str) -> Result<String, RpcE
let host = json::get_str(params, "vhost")
.or_else(|| json::get_str(params, "host"))
.ok_or_else(|| RpcError::invalid_params("missing 'vhost'"))?;
if !crate::users::valid_host(&host) {
return Err(RpcError::invalid_params("invalid vhost"));
}
s.change_host_ident(uid, None, Some(&host));
Ok(obj(&[("result", "true".into())]))
}
@ -114,6 +117,16 @@ pub fn handle(s: &mut Server, action: &str, params: &str) -> Result<String, RpcE
let uid = resolve(s, params).ok_or_else(|| RpcError::not_found("no such user"))?;
let newnick = json::get_str(params, "newnick")
.ok_or_else(|| RpcError::invalid_params("missing 'newnick'"))?;
// validate + collision-check like the NICK / SVSNICK paths: an invalid or
// taken nick would otherwise inject into the wire or hijack the nick index
if !crate::users::valid_nick(&newnick, s.conf_num("maxnick", 30usize)) {
return Err(RpcError::invalid_params("invalid nick"));
}
if s.find_nick(&newnick).is_some_and(|o| o != uid)
|| s.remote_nick.contains_key(&newnick.to_ascii_lowercase())
{
return Err(RpcError::invalid_params("nick in use"));
}
s.set_nick(uid, &newnick);
Ok(obj(&[("result", "true".into())]))
}