Skip to content

Account enumeration paths that do not go through SpecialPage::run() or ApiQuery #24

Description

@JeroenDeDauw

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions