From a09dfc74df4d3d8c4130148ffc8651b75b92243b Mon Sep 17 00:00:00 2001 From: reverse Date: Wed, 19 Aug 2026 00:41:47 +0000 Subject: [PATCH] =?UTF-8?q?auth:=20constant-time=20compare=20for=20VHOST?= =?UTF-8?q?=20and=20WEBIRC=20secrets=20=E2=80=94=20both=20used=20plain=20?= =?UTF-8?q?=3D=3D=20on=20the=20config=20password,=20unlike=20oper/RPC/JWT?= =?UTF-8?q?=20secrets=20which=20already=20route=20through=20ct=5Feq;=20exp?= =?UTF-8?q?ose=20password=5Fhash::ct=5Feq=20as=20the=20shared=20comparator?= =?UTF-8?q?=20and=20use=20it=20(usernames=20stay=20plain=20=3D=3D=20?= =?UTF-8?q?=E2=80=94=20not=20secret)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/coremods/core_user.rs | 13 +++++++++++-- src/modules/password_hash.rs | 3 ++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/coremods/core_user.rs b/src/coremods/core_user.rs index 659471b..c7ab02b 100644 --- a/src/coremods/core_user.rs +++ b/src/coremods/core_user.rs @@ -43,7 +43,12 @@ impl Command for Vhost { let host = s.conf_all("vhost").iter().find_map(|line| { let mut it = line.split_whitespace(); match (it.next(), it.next(), it.next()) { - (Some(u), Some(p), Some(h)) if u == user && p == pass => Some(h.to_string()), + (Some(u), Some(p), Some(h)) + if u == user + && crate::modules::password_hash::ct_eq(p.as_bytes(), pass.as_bytes()) => + { + Some(h.to_string()) + } _ => None, } }); @@ -104,7 +109,11 @@ impl Command for WebIrc { let Some(gw) = s .webirc .iter() - .find(|g| g.password == *pass && !g.ipmask.is_empty() && glob_match(&g.ipmask, &from)) + .find(|g| { + crate::modules::password_hash::ct_eq(g.password.as_bytes(), pass.as_bytes()) + && !g.ipmask.is_empty() + && glob_match(&g.ipmask, &from) + }) .map(|g| g.name.clone()) else { s.notice_star(uid, "WEBIRC: invalid credentials"); diff --git a/src/modules/password_hash.rs b/src/modules/password_hash.rs index 1b0b602..d1d46e9 100644 --- a/src/modules/password_hash.rs +++ b/src/modules/password_hash.rs @@ -53,7 +53,8 @@ fn unhex(s: &str) -> Option> { /// Constant-time equality (guards against timing attacks on the compare). /// `openssl::memcmp::eq` requires equal-length inputs, so short-circuit first. -fn ct_eq(a: &[u8], b: &[u8]) -> bool { +/// Shared comparator for any secret check (passwords, gateway/vhost secrets). +pub fn ct_eq(a: &[u8], b: &[u8]) -> bool { a.len() == b.len() && openssl::memcmp::eq(a, b) }