diff --git a/modules/chanserv/src/flags.rs b/modules/chanserv/src/flags.rs index 78b5edd..0b00ebe 100644 --- a/modules/chanserv/src/flags.rs +++ b/modules/chanserv/src/flags.rs @@ -56,6 +56,13 @@ pub fn handle(me: &str, from: &Sender, chan: &str, args: &[&str], ctx: &mut Serv return; } }; + // The `a` flag delegates access-list management, but only the founder may grant + // `f` (founder/co-founder) — otherwise a delegate could mint a founder-equivalent + // entry (Rank::Founder) and seize the channel. + if !is_founder && updated.has(echo_api::Flag::Founder) { + ctx.notice(me, from.uid, format!("Only the founder can grant the \x02f\x02 flag on \x02{chan}\x02.")); + return; + } if updated.is_empty() { match db.access_del(chan, target) { Ok(true) => ctx.notice(me, from.uid, format!("Cleared \x02{target}\x02's access to \x02{chan}\x02.")), diff --git a/src/engine/tests.rs b/src/engine/tests.rs index 7e736a7..bd6bc9e 100644 --- a/src/engine/tests.rs +++ b/src/engine/tests.rs @@ -4881,6 +4881,11 @@ // Grant carol the 'a' flag; now she can. cs(&mut e, "000AAAAAA", "FLAGS #room carol +a"); assert!(has(&cs(&mut e, "000AAAAAC", "FLAGS #room bob +i"), "now holds"), "carol with 'a' can change flags"); + // ...but a non-founder delegate may NOT grant the founder flag (escalation). + assert!(has(&cs(&mut e, "000AAAAAC", "FLAGS #room bob +f"), "Only the founder"), "carol can't grant +f"); + assert!(!e.db.channel("#room").unwrap().access.iter().any(|a| a.account.eq_ignore_ascii_case("bob") && a.level.contains('f')), "bob did not get founder"); + // The founder still can. + assert!(has(&cs(&mut e, "000AAAAAA", "FLAGS #room bob +f"), "now holds"), "founder can grant +f"); // An invalid flag letter is rejected. assert!(has(&cs(&mut e, "000AAAAAA", "FLAGS #room bob +z"), "isn't a valid flag"), "bad flag rejected");