Closing the account-listing special pages (#23) covered every
path that runs through SpecialPage::run(). Three paths that name or confirm accounts do not run through it, or
through ApiQuery, so neither the special page handler nor UserListApiHandler sees them. A member's username is
their email address, so each one is a way to read the roster.
Special page subpage autocomplete enumerates usernames
PrefixSearch::specialSearch() asks the special page object for completions directly:
$special = $spFactory->getPage( $specialTitle->getText() );
if ( $special ) {
$subpages = $special->prefixSearchSubpages( $subpageSearch, $limit, $offset );
SpecialPage::run() is never called, so SpecialPageBeforeExecute cannot fire and blocking the page has no effect
here. Eight core special pages answer with a username lookup — UserRights, Contributions,
DeletedContributions, Block, Unblock, EmailUser, ListFiles and RenameUser — each calling
UserNamePrefixSearch::search( AUDIENCE_PUBLIC, … ), a LIKE on user_name that honours $offset. Walking the
alphabet returns the roster. It is reachable anonymously through action=opensearch, list=prefixsearch,
generator=prefixsearch and GET /rest.php/v1/search/title, the last being what the search box itself calls.
This is enumeration rather than a guess-oracle, which makes it the largest of the three.
action=feedcontributions resolves account IDs
ApiFeedContributions allows id among its user types:
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'cidr', 'id', 'interwiki' ],
action=feedcontributions&user=%23<id> puts the resolved name in the feed's <link> before any items, so accounts
that have never edited resolve too. It is the same ID walk Special:Redirect was closed for, and it is not an
ApiQuery, so the blocked query submodules do not reach it.
action=validatepassword confirms an address
ApiValidatePassword answers userexists for a name already taken, without a token or any right. Since usernames
are addresses, it turns a list of addresses into a list of members.
Weaker oracles
Worth settling together with the above, none of them enumerating by itself: Special:Contributions/<name> answers
contributions-userdoesnotexist for an account that does not exist; action=emailuser probes the target before it
checks sendemail, so revoking that right does not close it; and after
#23, Special:UserRights/<name> still distinguishes an
existing account from a missing one.
Direction
The subpage autocomplete needs SpecialPage_initList subclasses whose prefixSearchSubpages() returns nothing for
the reader group, since the hook the extension already uses cannot reach it. The two actions need a blocked list of
top-level API actions next to $wgMemberAccessBlockedApiModules, which only covers query submodules.
Open question that shapes all of it
The extension gates on membership of the reader group, so an anonymous visitor keeps Special:ListUsers,
list=allusers and everything above. That is deliberate for a wiki that restricts reading, which the README says
is a separate decision, but it sits badly with the promise that nothing gives the member list away: a wiki that
loads the extension and leaves reading public hands the roster to anyone. RegistrationHandler already gates the
log restrictions on the memberaccess-manage right instead. Whether both handlers should move to that gate is a
decision to take before the extension ships, and it decides what the fixes above check.
AI-authored — Claude Code, Fable 5 (max); found by an Opus 5 (max) review subagent reviewing
#23, no steering beyond the review brief; text not yet
human-reviewed; every mechanism above re-checked against MediaWiki 1.43 source before filing, including which
eight special pages carry the autocomplete, and the reviewer reproduced the autocomplete and the ID walk
anonymously against a live wiki.
Closing the account-listing special pages (#23) covered every
path that runs through
SpecialPage::run(). Three paths that name or confirm accounts do not run through it, orthrough
ApiQuery, so neither the special page handler norUserListApiHandlersees them. A member's username istheir email address, so each one is a way to read the roster.
Special page subpage autocomplete enumerates usernames
PrefixSearch::specialSearch()asks the special page object for completions directly:SpecialPage::run()is never called, soSpecialPageBeforeExecutecannot fire and blocking the page has no effecthere. Eight core special pages answer with a username lookup —
UserRights,Contributions,DeletedContributions,Block,Unblock,EmailUser,ListFilesandRenameUser— each callingUserNamePrefixSearch::search( AUDIENCE_PUBLIC, … ), aLIKEonuser_namethat honours$offset. Walking thealphabet returns the roster. It is reachable anonymously through
action=opensearch,list=prefixsearch,generator=prefixsearchandGET /rest.php/v1/search/title, the last being what the search box itself calls.This is enumeration rather than a guess-oracle, which makes it the largest of the three.
action=feedcontributions resolves account IDs
ApiFeedContributionsallowsidamong its user types:action=feedcontributions&user=%23<id>puts the resolved name in the feed's<link>before any items, so accountsthat have never edited resolve too. It is the same ID walk
Special:Redirectwas closed for, and it is not anApiQuery, so the blocked query submodules do not reach it.action=validatepassword confirms an address
ApiValidatePasswordanswersuserexistsfor a name already taken, without a token or any right. Since usernamesare addresses, it turns a list of addresses into a list of members.
Weaker oracles
Worth settling together with the above, none of them enumerating by itself:
Special:Contributions/<name>answerscontributions-userdoesnotexistfor an account that does not exist;action=emailuserprobes the target before itchecks
sendemail, so revoking that right does not close it; and after#23,
Special:UserRights/<name>still distinguishes anexisting account from a missing one.
Direction
The subpage autocomplete needs
SpecialPage_initListsubclasses whoseprefixSearchSubpages()returns nothing forthe reader group, since the hook the extension already uses cannot reach it. The two actions need a blocked list of
top-level API actions next to
$wgMemberAccessBlockedApiModules, which only covers query submodules.Open question that shapes all of it
The extension gates on membership of the reader group, so an anonymous visitor keeps
Special:ListUsers,list=allusersand everything above. That is deliberate for a wiki that restricts reading, which the README saysis a separate decision, but it sits badly with the promise that nothing gives the member list away: a wiki that
loads the extension and leaves reading public hands the roster to anyone.
RegistrationHandleralready gates thelog restrictions on the
memberaccess-manageright instead. Whether both handlers should move to that gate is adecision to take before the extension ships, and it decides what the fixes above check.