Harden deferred audit LOWs: provisioned-verifier cost cap, BOT-nick validation, gameserv board-delivery account guard, empty KICK/PART/QUIT reason parse, and relink SASL/enforce cleanup
All checks were successful
CI / check (push) Successful in 5m4s

This commit is contained in:
Jean Chevronnet 2026-07-20 04:40:42 +00:00
parent 1b569fdfa3
commit 4027242c1e
No known key found for this signature in database
13 changed files with 144 additions and 19 deletions

View file

@ -195,7 +195,7 @@ impl Protocol for InspIrcd {
// :<uid> PART <chan> [:reason] — a user leaving a channel.
"PART" => match (source.as_deref(), tokens.next()) {
(Some(uid), Some(chan)) if chan.starts_with('#') => {
vec![NetEvent::Part { uid: uid.to_string(), channel: chan.to_string(), reason: trailing(rest) }]
vec![NetEvent::Part { uid: uid.to_string(), channel: chan.to_string(), reason: reason(rest) }]
}
_ => vec![],
},
@ -204,7 +204,7 @@ impl Protocol for InspIrcd {
let chan = tokens.next().unwrap_or("");
match tokens.next() {
Some(uid) if chan.starts_with('#') => {
vec![NetEvent::Kicked { channel: chan.to_string(), uid: uid.to_string(), by: source.unwrap_or_default(), reason: trailing(rest) }]
vec![NetEvent::Kicked { channel: chan.to_string(), uid: uid.to_string(), by: source.unwrap_or_default(), reason: reason(rest) }]
}
_ => vec![],
}
@ -291,7 +291,7 @@ impl Protocol for InspIrcd {
_ => vec![],
}
}
"QUIT" => vec![NetEvent::Quit { uid: source.unwrap_or_default(), reason: trailing(rest) }],
"QUIT" => vec![NetEvent::Quit { uid: source.unwrap_or_default(), reason: reason(rest) }],
// :<src> KILL <uid> :<reason> — a user forcibly removed. We forget
// them (or reintroduce, if it was one of our bots).
"KILL" => match tokens.next() {
@ -683,6 +683,17 @@ fn trailing(rest: &str) -> String {
}
}
// A free-text reason param (PART/KICK/QUIT), which the ircd always sends as a
// `:`-prefixed trailing. Unlike `trailing()`, a MISSING reason yields "" rather
// than the last preceding word — so `KICK #c target` (no reason) doesn't record
// the target uid as the reason.
fn reason(rest: &str) -> String {
match rest.find(" :") {
Some(i) => rest[i + 2..].to_string(),
None => String::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -691,6 +702,24 @@ mod tests {
InspIrcd::new("services.test".into(), "Federated Services".into(), "42S".into(), "pw".into(), 1206, 1, "iHkBT".into())
}
// A KICK/PART/QUIT with no explicit `:reason` must record an EMPTY reason,
// not the last preceding word (a channel/target token).
#[test]
fn kick_without_reason_is_empty_not_the_target() {
let ev = proto().parse(":42SAAAAAB KICK #chan 0IRAAAAAB");
assert!(
matches!(ev.as_slice(), [NetEvent::Kicked { reason, .. }] if reason.is_empty()),
"{ev:?}"
);
let ev = proto().parse(":42SAAAAAB KICK #chan 0IRAAAAAB :flooding");
assert!(
matches!(ev.as_slice(), [NetEvent::Kicked { reason, .. }] if reason == "flooding"),
"{ev:?}"
);
let ev = proto().parse(":0IRAAAAAB QUIT");
assert!(matches!(ev.as_slice(), [NetEvent::Quit { reason, .. }] if reason.is_empty()), "{ev:?}");
}
// A remote nick change must surface as a NickChange for the source uid, or
// nick-based commands act on a stale nick.
#[test]