PPWP – Password Protect Pages <= 1.9.15 - Missing Authorization
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:NTechnical Details
<=1.9.15What Changed in the Fix
Changes introduced in v1.9.16
Source Code
WordPress.org SVN# 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_passwordnonce: 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
- Entry Point: A logged-in user sends a request to
admin-ajax.phpwith the actionppw_set_password. - Hook Registration: The plugin registers
ppw_free_set_passwordas an AJAX handler (typically in the main plugin file or a loader, though not shown in the snippet, the naming convention inPPW_Adminconfirms its purpose). - Nonce Validation:
ppw_free_set_passwordcallsppw_free_error_before_create_password($_REQUEST, $setting_keys). This function verifies the nonce provided in the request. - Missing Check: Immediately following the nonce check, the function proceeds to extract
$data_settingsfrom$_REQUEST['settings']and calls$free_services->create_new_password(). - Execution: The
create_new_passwordmethod updates the post meta (storing the password) for the ID provided inid_page_postwithout checking if the current user hasedit_postcapabilities 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.
- Access: A Contributor can access the WordPress dashboard and navigate to
wp-admin/post-new.phpor edit one of their own posts. - Shortcode/Metabox: The metabox logic is defined in
ppw_free_add_custom_meta_box_to_edit_page(includesview-ppw-meta-box.php). - Extraction:
- Use
browser_navigatetowp-admin/post-new.php. - Use
browser_evalto extract the nonce. - JS Variable: The plugin often localizes data into an object named
ppw_free_adminorppwp_data. - Metabox Field: Alternatively, the nonce is likely in a hidden input field within the
ppw-meta-boxdiv. - Command:
browser_eval("document.querySelector('#ppw_nonce')?.value || document.querySelector('input[name*=\"nonce\"]')?.value").
- Use
5. Exploitation Strategy
- Login: Authenticate as a Contributor user.
- Nonce Retrieval:
- Navigate to
wp-admin/post-new.php. - Extract the nonce value associated with the PPWP metabox.
- Navigate to
- Target Identification: Identify the ID of a published Administrator post (e.g., Post ID 1).
- 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
- Send a POST request to
- 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
- 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). - Contributor User: Create a Contributor user:
wp user create attacker attacker@example.com --role=contributor --user_pass=password. - Plugin Activation: Ensure
password-protect-pageis 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
- 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_). - Verify Protection: Attempt to access the post URL unauthenticated via
http_request. The response should contain the PPWP password form (look for stringppw_passwordorPassword Protected).
9. Alternative Approaches
- Role-Based Bypass: If
is_role_selectedis 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_passwordarray to see if it allows overriding complex protection schemes. - Action Guessing: If
ppw_set_passwordfails, check the page source forwp_localize_scriptto find the exactactionstring used by the plugin'sadmin.js. Common variants:ppw_free_set_password,ppwp_save_settings.
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
@@ -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.