OperServ: enforce FORBID EMAIL at registration and SET EMAIL

FORBID accepted and stored EMAIL patterns, but nothing ever checked them:
a forbidden address still registered and could be set via NickServ SET
EMAIL. Both paths now reject a matching address (new ForbiddenEmail
outcome), matching how NICK and CHAN forbids already gate registration.
This commit is contained in:
Jean Chevronnet 2026-07-16 03:11:57 +00:00
parent 01ea0fa95e
commit b749cd2969
No known key found for this signature in database
4 changed files with 28 additions and 0 deletions

View file

@ -29,6 +29,12 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
Some("EMAIL") => { Some("EMAIL") => {
let email = args.get(2).map(|s| s.to_string()); let email = args.get(2).map(|s| s.to_string());
let cleared = email.is_none(); let cleared = email.is_none();
if let Some(addr) = &email {
if db.is_forbidden("EMAIL", addr).is_some() {
ctx.notice(me, from.uid, "That email address is forbidden by network policy. Use a different one.");
return;
}
}
match db.set_email(account, email) { match db.set_email(account, email) {
Ok(()) if cleared => ctx.notice(me, from.uid, format!("Email for \x02{account}\x02 cleared.")), Ok(()) if cleared => ctx.notice(me, from.uid, format!("Email for \x02{account}\x02 cleared.")),
Ok(()) => ctx.notice(me, from.uid, format!("Email for \x02{account}\x02 updated.")), Ok(()) => ctx.notice(me, from.uid, format!("Email for \x02{account}\x02 updated.")),

View file

@ -1268,6 +1268,8 @@ enum RegOutcome {
VerifyRequired, VerifyRequired,
/// The name is on the OperServ FORBID list. /// The name is on the OperServ FORBID list.
Forbidden, Forbidden,
/// The supplied email matches an OperServ FORBID EMAIL pattern.
ForbiddenEmail,
Exists, Exists,
RateLimited, RateLimited,
Frozen, Frozen,
@ -1284,6 +1286,7 @@ fn reg_reply(reply: &RegReply, outcome: RegOutcome, account: &str) -> Vec<NetAct
RegOutcome::Ok => ("success", "*", "Account registered."), RegOutcome::Ok => ("success", "*", "Account registered."),
RegOutcome::VerifyRequired => ("verification_required", "VERIFICATION_REQUIRED", "Registered — check your email for a code, then VERIFY."), RegOutcome::VerifyRequired => ("verification_required", "VERIFICATION_REQUIRED", "Registered — check your email for a code, then VERIFY."),
RegOutcome::Forbidden => ("error", "BAD_ACCOUNT_NAME", "That account name is forbidden by network policy."), RegOutcome::Forbidden => ("error", "BAD_ACCOUNT_NAME", "That account name is forbidden by network policy."),
RegOutcome::ForbiddenEmail => ("error", "BAD_EMAIL", "That email address is forbidden by network policy."),
RegOutcome::Exists => ("error", "ACCOUNT_EXISTS", "That account name is already registered."), RegOutcome::Exists => ("error", "ACCOUNT_EXISTS", "That account name is already registered."),
RegOutcome::RateLimited => ("error", "TEMPORARILY_UNAVAILABLE", "Too many registrations, please wait a moment."), RegOutcome::RateLimited => ("error", "TEMPORARILY_UNAVAILABLE", "Too many registrations, please wait a moment."),
RegOutcome::Frozen => ("error", "TEMPORARILY_UNAVAILABLE", "Registrations are temporarily frozen by network staff."), RegOutcome::Frozen => ("error", "TEMPORARILY_UNAVAILABLE", "Registrations are temporarily frozen by network staff."),
@ -1311,6 +1314,7 @@ fn reg_reply(reply: &RegReply, outcome: RegOutcome, account: &str) -> Vec<NetAct
notice(format!("Your nick \x02{nick}\x02 is now registered and you're logged in. Welcome!")), notice(format!("Your nick \x02{nick}\x02 is now registered and you're logged in. Welcome!")),
], ],
RegOutcome::Forbidden => vec![notice("That nickname is forbidden and can't be registered.".to_string())], RegOutcome::Forbidden => vec![notice("That nickname is forbidden and can't be registered.".to_string())],
RegOutcome::ForbiddenEmail => vec![notice("That email address is forbidden by network policy. Use a different one.".to_string())],
RegOutcome::Exists => vec![notice(format!("\x02{nick}\x02 is already registered. If it's yours, use \x02IDENTIFY <password>\x02."))], RegOutcome::Exists => vec![notice(format!("\x02{nick}\x02 is already registered. If it's yours, use \x02IDENTIFY <password>\x02."))],
RegOutcome::RateLimited => vec![notice("Registrations are busy right now. Please try again in a moment.".to_string())], RegOutcome::RateLimited => vec![notice("Registrations are busy right now. Please try again in a moment.".to_string())],
RegOutcome::Frozen => vec![notice("Registrations are temporarily frozen by network staff. Please try again later.".to_string())], RegOutcome::Frozen => vec![notice("Registrations are temporarily frozen by network staff. Please try again later.".to_string())],

View file

@ -220,6 +220,12 @@ impl Engine {
let Some(creds) = creds else { let Some(creds) = creds else {
return reg_reply(&reply, RegOutcome::Internal, account); return reg_reply(&reply, RegOutcome::Internal, account);
}; };
// A forbidden email pattern (OperServ FORBID EMAIL) blocks registration.
if let Some(addr) = &email {
if self.db.is_forbidden("EMAIL", addr).is_some() {
return reg_reply(&reply, RegOutcome::ForbiddenEmail, account);
}
}
let addr = email.clone(); let addr = email.clone();
let outcome = match self.db.register_prepared(account, creds, email) { let outcome = match self.db.register_prepared(account, creds, email) {
Ok(()) if self.db.is_verified(account) => RegOutcome::Ok, Ok(()) if self.db.is_verified(account) => RegOutcome::Ok,

View file

@ -1224,6 +1224,18 @@
assert!(e.pre_register_check("cleanname", &reply).is_none(), "clean nick allowed"); assert!(e.pre_register_check("cleanname", &reply).is_none(), "clean nick allowed");
assert!(e.db.is_forbidden("CHAN", "#warez").is_some(), "channel is forbidden"); assert!(e.db.is_forbidden("CHAN", "#warez").is_some(), "channel is forbidden");
// EMAIL forbids block registration and SET EMAIL.
assert!(notice(&os(&mut e, "FORBID ADD EMAIL *@spam.tld disposable"), "Forbade"), "email forbid added");
let ereply = || crate::proto::RegReply::NickServ { agent: "42SAAAAAA".into(), uid: "000AAAAAE".into(), nick: "newbie".into() };
let out = e.complete_register("newbie", Db::derive_credentials("pw", 4096), Some("evil@spam.tld".into()), ereply());
assert!(notice(&out, "email address is forbidden"), "forbidden email blocks registration: {out:?}");
assert!(!e.db.exists("newbie"), "no account created with a forbidden email");
e.complete_register("newbie", Db::derive_credentials("pw", 4096), Some("ok@good.tld".into()), ereply());
assert!(e.db.exists("newbie"), "a clean email registers fine");
// boss (identified) can't SET EMAIL to a forbidden address either.
let out = e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAA".into(), text: "SET EMAIL nope@spam.tld".into() });
assert!(notice(&out, "email address is forbidden"), "SET EMAIL rejects a forbidden address: {out:?}");
assert!(notice(&os(&mut e, "FORBID LIST"), "evil*"), "list shows the nick forbid"); assert!(notice(&os(&mut e, "FORBID LIST"), "evil*"), "list shows the nick forbid");
assert!(notice(&os(&mut e, "FORBID DEL NICK evil*"), "Removed"), "nick forbid removed"); assert!(notice(&os(&mut e, "FORBID DEL NICK evil*"), "Removed"), "nick forbid removed");
assert!(e.pre_register_check("evilbob", &reply).is_none(), "no longer forbidden"); assert!(e.pre_register_check("evilbob", &reply).is_none(), "no longer forbidden");