CVE-2026-32562

PPWP – Password Protect Pages <= 1.9.15 - Missing Authorization

mediumMissing Authorization
4.3
CVSS Score
4.3
CVSS Score
medium
Severity
1.9.16
Patched in
11d
Time to patch

Description

The PPWP – Password Protect Pages plugin for WordPress is vulnerable to unauthorized access due to a missing capability check on a function in all versions up to, and including, 1.9.15. This makes it possible for authenticated attackers, with Contributor-level access and above, to perform an unauthorized action.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
None
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=1.9.15
PublishedMarch 23, 2026
Last updatedApril 2, 2026
Affected pluginpassword-protect-page

What Changed in the Fix

Changes introduced in v1.9.16

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan - CVE-2026-32562 ## 1. Vulnerability Summary The **PPWP – Password Protect Pages** plugin (versions <= 1.9.15) contains a missing authorization vulnerability in its password-setting functionality. The function `PPW_Admin::ppw_free_set_password` (located in `admin/class-…

Show full research plan

Exploitation Research Plan - CVE-2026-32562

1. Vulnerability Summary

The PPWP – Password Protect Pages plugin (versions <= 1.9.15) contains a missing authorization vulnerability in its password-setting functionality. The function PPW_Admin::ppw_free_set_password (located in admin/class-ppw-admin.php) is responsible for saving password protection settings for posts and pages. While it performs a nonce check (via the helper ppw_free_error_before_create_password), it fails to verify if the current user has the authority to edit the post specified in the request or the capability to manage plugin settings. This allows an authenticated attacker with Contributor-level permissions to modify password protection settings for any post, including those authored by Administrators.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: ppw_set_password (inferred from function name and common plugin patterns)
  • HTTP Method: POST
  • Authentication: Required (Contributor-level or higher)
  • Vulnerable Parameter: settings[id_page_post] (allows specifying any Post ID)
  • Payload Parameters:
    • action: ppw_set_password
    • nonce: A valid security nonce (retrieved from the post metabox)
    • settings[save_password]: The new password to set for the post.
    • settings[id_page_post]: The target Post ID (e.g., an Administrator's post).
    • settings[is_role_selected]: Boolean/String indicating if role-based protection is active.
    • settings[ppwp_multiple_password][]: Array of passwords (if using multiple).

3. Code Flow

  1. Entry Point: A logged-in user sends a request to admin-ajax.php with the action ppw_set_password.
  2. Hook Registration: The plugin registers ppw_free_set_password as an AJAX handler (typically in the main plugin file or a loader, though not shown in the snippet, the naming convention in PPW_Admin confirms its purpose).
  3. Nonce Validation: ppw_free_set_password calls ppw_free_error_before_create_password($_REQUEST, $setting_keys). This function verifies the nonce provided in the request.
  4. Missing Check: Immediately following the nonce check, the function proceeds to extract $data_settings from $_REQUEST['settings'] and calls $free_services->create_new_password().
  5. Execution: The create_new_password method updates the post meta (storing the password) for the ID provided in id_page_post without checking if the current user has edit_post capabilities for that specific ID.

4. Nonce Acquisition Strategy

The nonce is required to pass the ppw_free_error_before_create_password check. The plugin enqueues a metabox on the post edit screen where this nonce is generated.

  1. Access: A Contributor can access the WordPress dashboard and navigate to wp-admin/post-new.php or edit one of their own posts.
  2. Shortcode/Metabox: The metabox logic is defined in ppw_free_add_custom_meta_box_to_edit_page (includes view-ppw-meta-box.php).
  3. Extraction:
    • Use browser_navigate to wp-admin/post-new.php.
    • Use browser_eval to extract the nonce.
    • JS Variable: The plugin often localizes data into an object named ppw_free_admin or ppwp_data.
    • Metabox Field: Alternatively, the nonce is likely in a hidden input field within the ppw-meta-box div.
    • Command: browser_eval("document.querySelector('#ppw_nonce')?.value || document.querySelector('input[name*=\"nonce\"]')?.value").

5. Exploitation Strategy

  1. Login: Authenticate as a Contributor user.
  2. Nonce Retrieval:
    • Navigate to wp-admin/post-new.php.
    • Extract the nonce value associated with the PPWP metabox.
  3. Target Identification: Identify the ID of a published Administrator post (e.g., Post ID 1).
  4. Malicious Request:
    • Send a POST request to /wp-admin/admin-ajax.php.
    • Headers: Content-Type: application/x-www-form-urlencoded.
    • Body:
      action=ppw_set_password&nonce=[NONCE]&settings[id_page_post]=[TARGET_ID]&settings[save_password]=pwned123&settings[is_role_selected]=0&settings[ppwp_multiple_password][]=pwned123
      
  5. Impact: The Administrator's post is now password-protected with "pwned123", potentially locking out legitimate visitors or altering the site's intended visibility.

6. Test Data Setup

  1. Admin Post: Create a post as the Administrator: wp post create --post_type=post --post_title="Admin Secret" --post_status=publish --post_author=1 (Note the returned ID, e.g., 5).
  2. Contributor User: Create a Contributor user: wp user create attacker attacker@example.com --role=contributor --user_pass=password.
  3. Plugin Activation: Ensure password-protect-page is active.

7. Expected Results

  • The AJAX request should return a JSON response (generated by wp_send_json( $current_roles_password )) containing the updated password details.
  • The HTTP response status should be 200 OK.
  • The target post should now be inaccessible without the password "pwned123".

8. Verification Steps

  1. Check Post Meta: Use WP-CLI to verify the password was set on the Admin's post:
    wp post generate --post_id=[TARGET_ID] --format=json
    Or specifically check the meta keys:
    wp post meta list [TARGET_ID] (Look for keys starting with _ppw_ or _pcp_).
  2. Verify Protection: Attempt to access the post URL unauthenticated via http_request. The response should contain the PPWP password form (look for string ppw_password or Password Protected).

9. Alternative Approaches

  • Role-Based Bypass: If is_role_selected is manipulated, the attacker might whitelist specific roles (like "contributor") to access admin-only content if the plugin uses these settings to override default WordPress permissions.
  • Multiple Passwords: Attempt to inject multiple passwords into the ppwp_multiple_password array to see if it allows overriding complex protection schemes.
  • Action Guessing: If ppw_set_password fails, check the page source for wp_localize_script to find the exact action string used by the plugin's admin.js. Common variants: ppw_free_set_password, ppwp_save_settings.
Research Findings
Static analysis — not yet PoC-verified

Summary

The PPWP – Password Protect Pages plugin for WordPress is vulnerable to unauthorized access because the AJAX function ppw_free_set_password fails to perform a capability check. This allows authenticated attackers with Contributor-level permissions or higher to modify the password protection settings of any post, including those authored by administrators.

Vulnerable Code

// admin/class-ppw-admin.php line 143
	public function ppw_free_set_password() {
		$setting_keys = array( 'save_password', 'id_page_post', 'is_role_selected', 'ppwp_multiple_password' );
		if ( ppw_free_error_before_create_password( $_REQUEST, $setting_keys ) ) {  // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- We handle nonce verification in this function
			wp_send_json(
				array(
					'is_error' => true,
					'message'  => PPW_Constants::BAD_REQUEST_MESSAGE,
				),
				400
			);
			wp_die();
		}
		if ( ! isset( $_REQUEST['settings'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- We handle nonce verification above.
			// ... (Truncated)
		}

		$data_settings        = $_REQUEST['settings']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- We no need to verify nonce for enqueue assets, Don't need use wp_unslash(), and no need to sanitize settings params.
		$new_role_password    = $data_settings['save_password'];
		$id                   = $data_settings['id_page_post'];
		$role_selected        = $data_settings['is_role_selected'];
		$new_global_passwords = is_array( $data_settings['ppwp_multiple_password'] ) ? $data_settings['ppwp_multiple_password'] : array();

		$free_services          = new PPW_Password_Services();
		$current_roles_password = $free_services->create_new_password( $id, $role_selected, $new_global_passwords, $new_role_password );
		wp_send_json( $current_roles_password );
		wp_die();
	}

Security Fix

--- /home/deploy/wp-safety.org/data/plugin-versions/password-protect-page/1.9.15/admin/class-ppw-admin.php	2025-07-17 07:02:26.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/password-protect-page/1.9.16/admin/class-ppw-admin.php	2026-03-19 11:27:32.000000000 +0000
@@ -171,6 +171,19 @@
 		$id                   = $data_settings['id_page_post'];
 		$role_selected        = $data_settings['is_role_selected'];
 		$new_global_passwords = is_array( $data_settings['ppwp_multiple_password'] ) ? $data_settings['ppwp_multiple_password'] : array();
+
+		if ( ! current_user_can( 'edit_post', $id ) ) {
+			wp_send_json(
+				array(
+					'is_error' => true,
+					'message'  => PPW_Constants::BAD_REQUEST_MESSAGE,
+				),
+				400
+			);
+
+			wp_die();
+		}
+
 		$free_services          = new PPW_Password_Services();
 		$current_roles_password = $free_services->create_new_password( $id, $role_selected, $new_global_passwords, $new_role_password );
 		wp_send_json( $current_roles_password );

Exploit Outline

To exploit this vulnerability, an authenticated attacker with Contributor-level access needs to perform the following steps: 1. Login to the WordPress dashboard as a Contributor. 2. Navigate to a post editing screen (like `wp-admin/post-new.php`) to retrieve a valid security nonce from the PPWP metabox (the plugin uses the same nonce logic for all posts). 3. Identify the target Post ID that the attacker wishes to lock or modify. 4. Send a POST request to `/wp-admin/admin-ajax.php` with the `action` parameter set to `ppw_set_password`. 5. In the `settings` array parameter, specify the target Post ID in `id_page_post` and the desired password in `save_password`. 6. The plugin will process the request and update the post meta associated with the target ID because it only verifies the nonce and not the user's authority to edit that specific post.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.