Fix duration overflow, expiry-warned resets, DICT control-char injection, and HostServ TAKE forbid recheck

This commit is contained in:
Jean Chevronnet 2026-07-19 13:24:46 +00:00
parent 68df1e7078
commit d833dc28f0
No known key found for this signature in database
5 changed files with 20 additions and 2 deletions

View file

@ -2354,7 +2354,9 @@ pub fn parse_duration(s: &str) -> Option<u64> {
c if c.is_ascii_digit() => (s, 1),
_ => return None,
};
num.parse::<u64>().ok().map(|n| n * mult)
// Checked so an out-of-range duration is rejected (None) rather than wrapping to
// a garbage/near-instant expiry in a release build.
num.parse::<u64>().ok().and_then(|n| n.checked_mul(mult))
}
// Case-insensitive hostmask glob: `*` (any run) and `?` (one char).

View file

@ -21,6 +21,12 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return;
}
};
// A menu offer can outlive a later FORBID; re-check at grant time like the
// request/approve/default paths, or a stale offer bypasses the impersonation guard.
if db.vhost_is_forbidden(&host) {
ctx.notice(me, from.uid, format!("\x02{host}\x02 isn't allowed here. Please choose another."));
return;
}
match db.set_vhost(account, &host, "offer", None) {
Ok(()) => {
ctx.apply_vhost(from.uid, &host);

View file

@ -96,7 +96,10 @@ fn parse_definition(raw: &str) -> Option<String> {
break;
}
}
let body: String = body.split_whitespace().collect::<Vec<_>>().join(" ");
// Drop control characters an untrusted/MITM'd DICT server could embed (CTCP
// \x01, color/format \x02\x03, NUL truncation) before this is spoken into a
// channel. split_whitespace already removed CR/LF/TAB.
let body: String = body.split_whitespace().collect::<Vec<_>>().join(" ").chars().filter(|c| !c.is_control()).collect();
if body.is_empty() {
return None;
}

View file

@ -734,6 +734,7 @@ impl Db {
let _ = self.log.append(Event::AccountSeen { account: account.to_string(), ts: now });
if let Some(a) = self.accounts.get_mut(&k) {
a.last_seen = a.last_seen.max(now);
a.expiry_warned = false; // mirror the AccountSeen fold, or a re-active account never re-warns
}
}
}
@ -747,6 +748,7 @@ impl Db {
let _ = self.log.append(Event::ChannelUsed { channel: channel.to_string(), ts: now });
if let Some(c) = self.channels.get_mut(&k) {
c.last_used = c.last_used.max(now);
c.expiry_warned = false; // mirror the ChannelUsed fold, or a re-active channel never re-warns
}
}
}

View file

@ -1258,6 +1258,11 @@ impl Db {
if c.noexpire {
snapshot.push(Event::ChannelNoExpire { channel: c.name.clone(), on: true });
}
if c.expiry_warned {
// Preserve the "warning already sent" flag, or compaction re-arms it
// and the idle channel is warned again on the next sweep.
snapshot.push(Event::ChannelExpiryWarned { channel: c.name.clone() });
}
if c.settings != ChanSettings::default() {
snapshot.push(Event::ChannelSettingsSet { channel: c.name.clone(), settings: c.settings });
}