View Issue Details

IDProjectCategoryView StatusLast Update
0006660unrealircdpublic2026-11-12 21:25
Reportermattf Assigned To 
PrioritynormalSeverityminorReproducibilityalways
Status newResolutionopen 
Product Version6.2.7.1 
Summary0006660: +D delay-join: hidden member's AWAY/NICK/QUIT, JOIN 0 and topic leak to the wrong users
DescriptionThis was found through an agentic-driven scan of the source and is part of a batch of non-critical issues I found worth reporting.

In a +D (delay-join) channel a member who has not spoken yet is hidden and should only be visible to channel staff. Four paths get that backwards or skip the check:

1. AWAY/NICK/QUIT. In src/send.c, sendto_local_common_channels() has:

    if (check_invisible && user_can_see_member(acptr, user, channel))
        continue;

The condition is inverted (there is a FIXME right above it): it skips the users who can see the hidden member and delivers to the ones who cannot. So a plain member who should not see the hidden user receives their AWAY/NICK/QUIT, and the staff who should see it do not.

2. JOIN 0. In src/modules/join.c the "JOIN 0 = leave all channels" path sends the PART with no visibility flag (sendto_channel with no CHECK_INVISIBLE), while a normal PART passes CHECK_INVISIBLE. So a hidden member who sends JOIN 0 reveals their PART to everyone.

3. TOPIC. In src/modules/topic.c the topic broadcast goes out without CHECK_INVISIBLE (the join path passes it, this one does not). A hidden member setting the topic reveals their nick!user@host, and the 333 reply replays it to later joiners.

4. Banned quit. In src/send.c, quit_sendto_local_common_channels() calls user_can_see_member_fast() with two arguments swapped relative to its signature (channel.c declares it as user, target, channel, target_member, user_member_modes). So when a hidden member is killed by a ban that carries a hidden reason (G-line/TKL/services kill), the check leaks their full nick!user@host and the quit reason to users who should not see them, and every relay server re-runs it, so the disclosure spreads network-wide.

Similar issues:
- https://bugs.unrealircd.org/view.php?id=5123
- https://bugs.unrealircd.org/view.php?id=5521
- https://bugs.unrealircd.org/view.php?id=6118
Steps To ReproduceA +D channel has three clients: an op (can see hidden members), a plain member who has already spoken (cannot see hidden members), and a third client who joins but stays silent (the hidden member).

1. The hidden member sends AWAY, changes nick, or quits. The plain member receives it; the op does not.
2. The hidden member sends JOIN 0. The plain member sees ":nick PART #chan :Left all channels".
3. On a +D -t channel, the hidden member sets the topic. The plain member receives the TOPIC line with the hidden member's nick!user@host, and a later joiner gets it again via 333.
4. On a +D channel, an oper G-lines (or TKL/services-kills) the hidden member. The plain member receives a QUIT with the hidden member's full nick!user@host and the ban reason.
Additional InformationSuggested fixes:
1. Add the missing "!": if (check_invisible && !user_can_see_member(acptr, user, channel)) continue;
2. Route the JOIN 0 PART through the same invisible-aware path as a normal PART (pass CHECK_INVISIBLE).
3. Pass CHECK_INVISIBLE on both topic broadcast sites.
4. Pass the leaver's member and the viewer's modes to user_can_see_member_fast() (the two are currently swapped).

Let me know if this is desired and I could PR it myself.
3rd party modules

Activities

There are no notes attached to this issue.

Issue History

Date Modified Username Field Change
2026-11-12 21:25 mattf New Issue