Members <= 3.2.22 - Unauthenticated Sensitive Information Disclosure via REST API Pagination Side Channel
Description
The Members – Membership & User Role Editor Plugin plugin for WordPress is vulnerable to Sensitive Information Exposure in all versions up to, and including, 3.2.22 via the members_filter_protected_posts_for_rest. This makes it possible for unauthenticated attackers to extract determine the existence and exact count of access-restricted posts, and use per-page pagination as a boolean oracle to infer keywords and content contained within those hidden restricted posts.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:NTechnical Details
What Changed in the Fix
Changes introduced in v3.2.23
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-12426 (Members Plugin) ## 1. Vulnerability Summary The **Members – Membership & User Role Editor** plugin (<= 3.2.22) for WordPress contains a sensitive information disclosure vulnerability. The vulnerability arises because the plugin's REST API filtering logi…
Show full research plan
Exploitation Research Plan: CVE-2026-12426 (Members Plugin)
1. Vulnerability Summary
The Members – Membership & User Role Editor plugin (<= 3.2.22) for WordPress contains a sensitive information disclosure vulnerability. The vulnerability arises because the plugin's REST API filtering logic, specifically within the members_filter_protected_posts_for_rest function (hooked into REST responses), fails to account for the metadata headers returned by WordPress.
While the plugin may successfully strip the content of a post from the JSON response if a user lacks the required roles, it does not modify the X-WP-Total and X-WP-TotalPages HTTP headers. These headers are calculated based on the initial WP_Query results. An unauthenticated attacker can use the search parameter to perform a "Boolean Search Oracle" attack, determining if specific keywords exist within restricted/private content by observing changes in the total post count reported in the headers.
2. Attack Vector Analysis
- Endpoint:
/wp-json/wp/v2/posts(Default) or any custom post type registered asshow_in_rest => true. - Method:
GET - Vulnerable Component:
members_filter_protected_posts_for_rest(Filter on REST response). - Attack Payload:
?search=KEYWORD&per_page=1 - Authentication: Unauthenticated (PR:N).
- Preconditions: At least one post must be restricted using the "Members" Content Permissions meta box (stored in
_members_access_rolemeta).
3. Code Flow
- Request Initiation: An unauthenticated user sends a GET request to
/wp-json/wp/v2/posts?search=secret. - Core Query: WordPress executes
WP_REST_Posts_Controller::get_items(). This triggers aWP_Queryfor all published posts containing "secret". - Header Generation: WordPress calculates the total number of matches and populates the
X-WP-Totalheader. - Plugin Intervention: The plugin's
members_filter_protected_posts_for_rest(inferred name from description) intercept the collection of post objects. - Permission Check: For each post, the plugin calls
members_can_current_user_view_post( $post_id ). - Information Leak: If the user is unauthenticated, the plugin removes the post data from the JSON body. However, the
X-WP-Totalheader remains unchanged from the count calculated in step 3.
4. Nonce Acquisition Strategy
The standard WordPress REST API GET requests for public post types (like posts and pages) do not require a nonce for unauthenticated access.
However, if the target site has restricted the REST API to authenticated users only, a nonce for the wp_rest action would be required.
- Strategy:
- Identify the plugin's "Login Form" widget or shortcode which might enqueue scripts.
- Create a page with the shortcode:
wp post create --post_type=page --post_status=publish --post_content='[members_login_form]' - Navigate to the page using
browser_navigate. - Since the
Manage_Rolesclass enqueuesmembers-admin, check for localized data:browser_eval("window.members_admin?.nonce")(inferred key).
- Note: In most default WordPress configurations, this exploit is nonce-less.
5. Exploitation Strategy
The goal is to use the X-WP-Total header as a side-channel to leak content from a restricted post.
Step 1: Baseline Check
Determine the current count of public posts.
- Request:
GET /wp-json/wp/v2/posts?per_page=1 - Tool:
http_request - Expected:
X-WP-Total: [N](where N is the count of public posts).
Step 2: Content Existence Oracle
Check if a specific "flag" (secret keyword) exists within any post (including restricted ones).
- Request:
GET /wp-json/wp/v2/posts?search=FLAG_VALUE&per_page=1 - Tool:
http_request - Observation: If
X-WP-Totalis greater than 0, but the JSON body[]is empty, the keyword exists only in restricted posts.
Step 3: Exact Count Extraction
If searching for a common word, the X-WP-Total header reveals exactly how many restricted posts contain that word.
Step 4: Content Brute-forcing (Oracle)
To extract a secret string (e.g., a "token" in a protected post):
- Request
?search=token_a->X-WP-Total: 0 - Request
?search=token_b->X-WP-Total: 1 - Result: The token starts with "b".
6. Test Data Setup
Perform the following using wp-cli:
- Enable Plugin: Ensure
membersis active. - Create Restricted Role:
wp role create restricted_member "Restricted Member" - Create Restricted Post:
wp post create --post_type=post --post_title="Restricted Sensitive Info" --post_content="The vault code is BlueMonkey77" --post_status=publish - Apply Restrictions:
Get the ID of the new post and apply the meta to restrict it to therestricted_memberrole:wp post meta add [POST_ID] _members_access_role restricted_member - Create Public Post:
wp post create --post_type=post --post_title="Public Welcome" --post_content="Welcome to the site" --post_status=publish
7. Expected Results
- Normal Request:
GET /wp-json/wp/v2/posts?search=WelcomereturnsX-WP-Total: 1and the post content in the body. - Exploit Request:
GET /wp-json/wp/v2/posts?search=BlueMonkey77returnsX-WP-Total: 1but an empty body[]. - Confirmation: The attacker now knows "BlueMonkey77" exists in a restricted post, which is a clear information disclosure.
8. Verification Steps
- Header Check: Verify the
X-WP-Totalheader is present and reflects the restricted post count.# After running the exploit tool Check if (response.headers['X-WP-Total'] == '1' && response.body == '[]') - Verify Meta: Ensure the post is truly restricted.
wp post meta get [POST_ID] _members_access_role
9. Alternative Approaches
- Author ID Discovery: If posts are restricted by author, use
?author=1,?author=2to see counts of restricted posts per author. - Categorization Leak: Use
?categories=[ID]to determine if restricted posts exist within specific sensitive categories. - Pagination Oracle: If
X-WP-Totalis suppressed by another plugin, use?page=X. Ifpage=1returns 200 OK butpage=2returns 400 Bad Request, the attacker can still binary-search the total count of matching restricted items.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.