CVE-2026-12426

Members <= 3.2.22 - Unauthenticated Sensitive Information Disclosure via REST API Pagination Side Channel

mediumExposure of Sensitive Information to an Unauthorized Actor
5.3
CVSS Score
5.3
CVSS Score
medium
Severity
3.2.23
Patched in
1d
Time to patch

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:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
Low
Confidentiality
None
Integrity
None
Availability

Technical Details

Affected versions<=3.2.22
PublishedJuly 10, 2026
Last updatedJuly 11, 2026
Affected pluginmembers

What Changed in the Fix

Changes introduced in v3.2.23

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# 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 as show_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_role meta).

3. Code Flow

  1. Request Initiation: An unauthenticated user sends a GET request to /wp-json/wp/v2/posts?search=secret.
  2. Core Query: WordPress executes WP_REST_Posts_Controller::get_items(). This triggers a WP_Query for all published posts containing "secret".
  3. Header Generation: WordPress calculates the total number of matches and populates the X-WP-Total header.
  4. Plugin Intervention: The plugin's members_filter_protected_posts_for_rest (inferred name from description) intercept the collection of post objects.
  5. Permission Check: For each post, the plugin calls members_can_current_user_view_post( $post_id ).
  6. Information Leak: If the user is unauthenticated, the plugin removes the post data from the JSON body. However, the X-WP-Total header 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:
    1. Identify the plugin's "Login Form" widget or shortcode which might enqueue scripts.
    2. Create a page with the shortcode: wp post create --post_type=page --post_status=publish --post_content='[members_login_form]'
    3. Navigate to the page using browser_navigate.
    4. Since the Manage_Roles class enqueues members-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-Total is 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):

  1. Request ?search=token_a -> X-WP-Total: 0
  2. Request ?search=token_b -> X-WP-Total: 1
  3. Result: The token starts with "b".

6. Test Data Setup

Perform the following using wp-cli:

  1. Enable Plugin: Ensure members is active.
  2. Create Restricted Role: wp role create restricted_member "Restricted Member"
  3. Create Restricted Post:
    wp post create --post_type=post --post_title="Restricted Sensitive Info" --post_content="The vault code is BlueMonkey77" --post_status=publish
    
  4. Apply Restrictions:
    Get the ID of the new post and apply the meta to restrict it to the restricted_member role:
    wp post meta add [POST_ID] _members_access_role restricted_member
    
  5. 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=Welcome returns X-WP-Total: 1 and the post content in the body.
  • Exploit Request: GET /wp-json/wp/v2/posts?search=BlueMonkey77 returns X-WP-Total: 1 but an empty body [].
  • Confirmation: The attacker now knows "BlueMonkey77" exists in a restricted post, which is a clear information disclosure.

8. Verification Steps

  1. Header Check: Verify the X-WP-Total header is present and reflects the restricted post count.
    # After running the exploit tool
    Check if (response.headers['X-WP-Total'] == '1' && response.body == '[]')
    
  2. 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=2 to 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-Total is suppressed by another plugin, use ?page=X. If page=1 returns 200 OK but page=2 returns 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.