Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : July 5, 2026

CVE-2026-57634: PPWP – Password Protect Pages <= 1.9.19 Authenticated (Contributor+) Insecure Direct Object Reference PoC, Patch Analysis & Rule

Severity Medium (CVSS 4.3)
CWE 639
Vulnerable Version 1.9.19
Patched Version 1.9.20
Disclosed June 25, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-57634: The PPWP (Password Protect Pages) plugin for WordPress, up to version 1.9.19, contains an Insecure Direct Object Reference (IDOR) vulnerability. This flaw allows authenticated attackers with Contributor-level access or higher to modify password protection settings for arbitrary posts and pages, including those they do not own or have permission to edit. The vulnerability impacts the admin interface’s password setting functionality, a password post status update handler, and REST API endpoints for password settings.

The root cause is the plugin’s failure to validate user capabilities before processing user-supplied post or page IDs. In the vulnerable code, the `ppw_free_set_password()` function in `class-ppw-admin.php` (lines 134-186) directly uses `$data_settings[‘id_page_post’]` without checking `current_user_can(‘edit_post’, $id)`. Similarly, the `update_post_status()` method (line 289+) and the REST API endpoints `get_pcp_settings` and `update_pcp_settings` in `class-ppw-api.php` (lines 836-870) accept an `id` parameter without verifying the user has edit permissions on that post. The patch introduces `absint()` sanitization followed by `current_user_can(‘edit_post’, $post_id)` checks in all these locations.

An attacker can exploit this by sending authenticated POST requests to `wp-admin/admin-ajax.php` with the action parameter set to `ppw_free_set_password` (or the relevant AJAX action), or by calling the REST API endpoints at `/wp-json/ppw/v1/settings` or `/wp-json/ppw/v1/update-settings`. The attacker supplies a targeted post ID (e.g., `id_page_post` or `id` parameter) that belongs to another user, along with new password configuration data. Because the plugin lacks authorization checks, the system processes the request and modifies the password settings for that post, effectively bypassing the intended access controls.

The patch adds authorization checks using `current_user_can(‘edit_post’, $id)` before any sensitive operation. In `ppw_free_set_password()`, the `$id` is now passed through `absint()` and then validated. The `update_post_status()` method now extracts `postId` from the request, sanitizes it, and checks permissions. The REST API endpoints similarly validate the `id` parameter. Without these checks, any authenticated user with edit capabilities (Contributor+) could manipulate password protection for any post. The fix ensures only users who can edit the specific post can modify its password settings.

The impact of this vulnerability is unauthorized modification of password-based access controls for posts and pages. An attacker with Contributor-level access could change or remove password protection from restricted content, gaining access to protected material. Conversely, they could set new passwords on posts they do not own, locking out legitimate authors or editors. This undermines the core functionality of the plugin, which is to provide controlled access to content. The CVSS score of 4.3 (Medium) reflects the requirement for authentication but the potential for significant data access manipulation.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/password-protect-page/admin/class-ppw-admin.php
+++ b/password-protect-page/admin/class-ppw-admin.php
@@ -134,7 +134,8 @@
 	 */
 	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
+		// phpcs:disable
+		if ( ppw_free_error_before_create_password( $_REQUEST, $setting_keys ) ) {
 			wp_send_json(
 				array(
 					'is_error' => true,
@@ -144,11 +145,8 @@
 			);
 			wp_die();
 		}
-		// phpcs:disable WordPress.Security.NonceVerification.Recommended
-		// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
-		// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash

-		if ( ! isset( $_REQUEST['settings'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- We handle nonce verification above.
+		if ( ! isset( $_REQUEST['settings'] ) ) {
 			wp_send_json(
 				array(
 					'is_error' => true,
@@ -163,10 +161,21 @@
 	    $data_settings = wp_unslash( $_REQUEST['settings'] );

 		$new_role_password    = $data_settings['save_password'];
-		$id                   = $data_settings['id_page_post'];
+		$id                   = absint( $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,
+				),
+				403
+			);
+			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 );
@@ -280,6 +289,17 @@
 			);
 			wp_die();
 		}
+		$post_id = isset( $_request['postId'] ) ? absint( $_request['postId'] ) : 0;
+		if ( ! $post_id || ! current_user_can( 'edit_post', $post_id ) ) {
+			wp_send_json(
+				array(
+					'is_error' => true,
+					'message'  => PPW_Constants::BAD_REQUEST_MESSAGE,
+				),
+				403
+			);
+			wp_die();
+		}

 		return $this->free_services->update_post_status( $_request );
 	}
--- a/password-protect-page/includes/class-ppw-api.php
+++ b/password-protect-page/includes/class-ppw-api.php
@@ -836,7 +836,10 @@

 		public function get_pcp_settings( $request ) {
 			$ppwp_db   = new PPW_Repository_Passwords();
-			$post_id   = $request->get_param( 'id' );
+			$post_id   = absint( $request->get_param( 'id' ) );
+			if ( ! $post_id || ! current_user_can( 'edit_post', $post_id ) ) {
+	       	 	return new WP_REST_Response( array( 'success' => false ), 403 );
+   		    }
 			$passwords = $ppwp_db->get_passwords_with_type_and_post_id( PPW_Content_Protection::PASSWORD_GLOBAL_TYPE, $post_id, 'password' );
 			$setting   = array();
 			if ( count( $passwords ) > 0 ) {
@@ -860,9 +863,11 @@
 		public function update_pcp_settings( $request ) {
 			$ppwp_db   = new PPW_Repository_Passwords();
 			$ppwp_area = new PPW_Content_Protection();
-			$post_id   = $request->get_param( 'id' );
+			$post_id   = absint( $request->get_param( 'id' ) );
 			$passwords = $request->get_param( 'passwords' );
-
+			if ( ! $post_id || ! current_user_can( 'edit_post', $post_id ) ) {
+		        return new WP_REST_Response( array( 'success' => false ), 403 );
+		    }
 			$post = get_post( $post_id );
 			if ( ! $ppwp_area->check_area_exist( $post ) ) {
 				return new WP_REST_Response(
--- a/password-protect-page/wp-protect-password.php
+++ b/password-protect-page/wp-protect-password.php
@@ -1,186 +1,187 @@
-<?php
-/**
- * The plugin bootstrap file
- *
- * This file is read by WordPress to generate the plugin information in the plugin
- * admin area. This file also includes all of the dependencies used by the plugin,
- * registers the activation and deactivation functions, and defines a function
- * that starts the plugin.
- *
- * @link              https://passwordprotectwp.com?utm_source=user-website&utm_medium=pluginsite_link&utm_campaign=ppwp
- * @since             1.7.6
- * @package           Password_Protect_Page
- *
- * @wordpress-plugin
- * Plugin Name:       Password Protect WordPress Lite
- * Plugin URI:        https://passwordprotectwp.com?utm_source=user-website&utm_medium=pluginsite_link&utm_campaign=ppwp_lite
- * Description:       Password protect the entire WordPress site, unlimited pages and posts by user roles. This plugin is required for our Pro version to work properly.
- * Version:           1.9.19
- * Author:            BWPS
- * Author URI:        https://passwordprotectwp.com
- * License:           GPL-2.0+
- * License URI:       http://www.gnu.org/licenses/gpl-2.0.txt
- * Text Domain:       password-protect-page
- * Domain Path:       /languages
- */
-
-// If this file is called directly, abort.
-if ( ! defined( 'WPINC' ) ) {
-	die;
-}
-
-/**
- * Currently plugin version.
- * Start at version 1.1.2 and use SemVer - https://semver.org
- * Rename this for your plugin and update it as you release new versions.
- */
-// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals
-define( 'PPW_VERSION', '1.9.19' );
-
-if ( ! defined( 'PPW_DIR_PATH' ) ) {
-	define( 'PPW_DIR_PATH', plugin_dir_path( __FILE__ ) );
-}
-
-if ( ! defined( 'PPW_DIR_URL' ) ) {
-	define( 'PPW_DIR_URL', plugin_dir_url( __FILE__ ) );
-}
-
-if ( ! defined( 'PPW_VIEW_URL' ) ) {
-	define( 'PPW_VIEW_URL', plugin_dir_url( __FILE__ ) . 'includes/views/' );
-}
-
-if ( ! defined( 'PPW_PLUGIN_NAME' ) ) {
-	define( 'PPW_PLUGIN_NAME', 'Password Protect WordPress Lite' );
-}
-
-if ( ! defined( 'PPW_PLUGIN_BASE_NAME' ) ) {
-	define( 'PPW_PLUGIN_BASE_NAME', plugin_basename( __FILE__ ) );
-}
-
-if ( ! defined( 'PPWP_SIDEBAR_API' ) ) {
-	define( 'PPWP_SIDEBAR_API', 'https://preventdirectaccess.com/wp-json/pda-fss-ppwp/v1/content' );
-}
-
-/**
- * The code that runs during plugin activation.
- * This action is documented in includes/class-ppw-activator.php
- */
-function activate_password_protect_page() {
-	require_once plugin_dir_path( __FILE__ ) . 'includes/class-ppw-activator.php';
-	PPW_Activator::activate();
-}
-
-/**
- * The code that runs during plugin deactivation.
- * This action is documented in includes/class-ppw-deactivator.php
- */
-function deactivate_password_protect_page() {
-	require_once plugin_dir_path( __FILE__ ) . 'includes/class-ppw-deactivator.php';
-	PPW_Deactivator::deactivate();
-}
-
-/**
- * The code that runs when uninstall plugin.
- */
-function uninstall_password_protect_page() {
-	require_once plugin_dir_path( __FILE__ ) . 'includes/class-ppw-uninstall.php';
-	PPW_Uninstall::uninstall();
-}
-
-register_activation_hook( __FILE__, 'activate_password_protect_page' );
-register_deactivation_hook( __FILE__, 'deactivate_password_protect_page' );
-register_uninstall_hook( __FILE__, 'uninstall_password_protect_page' );
-
-/**
- * The core plugin class that is used to define internationalization,
- * admin-specific hooks, and public-facing site hooks.
- */
-require plugin_dir_path( __FILE__ ) . 'includes/class-ppw.php';
-
-/**
- * Begins execution of the plugin
- *
- * Since everything within the plugin is registered via hooks,
- * then kicking off the plugin from this point in the file does
- * not affect the page life cycle.
- *
- * @since    1.1.2
- */
-function run_password_protect_page() {
-	$plugin = new Password_Protect_Page();
-	$plugin->run();
-}
-
-do_action( 'ppw_free/loaded' );
-
-add_action( 'plugins_loaded', 'ppw_check_environment_compatibility' );
-
-/**
- * Check PHP & WordPress version compatibility.
- */
-function ppw_check_environment_compatibility() {
-	if ( ! version_compare( PHP_VERSION, '5.6', '>=' ) ) {
-		add_action( 'admin_notices', 'ppw_fail_php_version' );
-	} elseif ( ! version_compare( get_bloginfo( 'version' ), '4.7', '>=' ) ) {
-		add_action( 'admin_notices', 'ppw_fail_wp_version' );
-	}
-}
-run_password_protect_page();
-
-
-add_action( 'plugins_loaded', 'ppw_free_load_plugin' );
-
-/**
- * Load migration service
- */
-function ppw_free_load_plugin() {
-	global $migration_free_service;
-	$migration_free_service = new PPW_Default_PW_Manager_Services();
-	global $password_recovery_service;
-	$password_recovery_service = new PPW_Password_Recovery_Manager();
-}
-
-/**
- * Function to check when PHP version is not supported.
- */
-function ppw_fail_php_version() {
-	/* translators: %s: PHP version */
-	$message      = sprintf( esc_html__( 'Password Protect WordPress requires PHP version %s+, plugin is currently NOT WORKING.', 'password-protect-page' ), '5.6' );
-	$html_message = sprintf( '<div class="error">%s</div>', wpautop( $message ) );
-	echo wp_kses_post( $html_message );
-}
-
-/**
- * Function to check when WP version is not supported.
- */
-function ppw_fail_wp_version() {
-	/* translators: %s: PHP version */
-	$message      = sprintf( esc_html__( 'Password Protect WordPress requires WordPress version %s+. Because you are using an earlier version, the plugin is currently NOT WORKING.', 'password-protect-page' ), '4.7' );
-	$html_message = sprintf( '<div class="error">%s</div>', wpautop( $message ) );
-	echo wp_kses_post( $html_message );
-}
-
-/* Plugin Analytics Data */
-function wpfolio_ppwp_analytics_load() {
-
-    require_once dirname( __FILE__ ) . '/wpfolio-analytics/wpfolio-analytics.php';
-
-    $wpfolio_analytics =  wpfolio_ppwp_anylc_init_module( array(
-                            'id'            => 11,
-                            'file'          => plugin_basename( __FILE__ ),
-                            'name'          => 'Password Protect WordPress',
-                            'slug'          => 'wp_protect_password_options',
-                            'tempslug'      => 'wp_protect_password_options_optin',
-                            'type'          => 'plugin',
-                            'menu'          => 'wp_protect_password_options',
-                            'redirect_page' => 'wp_protect_password_options',
-                            'text_domain'   => 'password-protect-page',
-                        ));
-
-    return $wpfolio_analytics;
-}
-
-// Init Analytics
-wpfolio_ppwp_analytics_load();
-
+<?php
+/**
+ * The plugin bootstrap file
+ *
+ * This file is read by WordPress to generate the plugin information in the plugin
+ * admin area. This file also includes all of the dependencies used by the plugin,
+ * registers the activation and deactivation functions, and defines a function
+ * that starts the plugin.
+ *
+ * @link              https://passwordprotectwp.com?utm_source=user-website&utm_medium=pluginsite_link&utm_campaign=ppwp
+ * @since             1.7.6
+ * @package           Password_Protect_Page
+ *
+ * @wordpress-plugin
+ * Plugin Name:       Password Protect WordPress Lite
+ * Plugin URI:        https://passwordprotectwp.com?utm_source=user-website&utm_medium=pluginsite_link&utm_campaign=ppwp_lite
+ * Description:       Password protect the entire WordPress site, unlimited pages and posts by user roles. This plugin is required for our Pro version to work properly.
+ * Version:           1.9.20
+ * Author:            BWPS
+ * Author URI:        https://passwordprotectwp.com
+ * License:           GPL-2.0+
+ * License URI:       http://www.gnu.org/licenses/gpl-2.0.txt
+ * Text Domain:       password-protect-page
+ * Domain Path:       /languages
+ */
+
+// If this file is called directly, abort.
+if ( ! defined( 'WPINC' ) ) {
+	die;
+}
+
+/**
+ * Currently plugin version.
+ * Start at version 1.1.2 and use SemVer - https://semver.org
+ * Rename this for your plugin and update it as you release new versions.
+ */
+// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals
+
+define( 'PPW_VERSION', '1.9.20' );
+
+if ( ! defined( 'PPW_DIR_PATH' ) ) {
+	define( 'PPW_DIR_PATH', plugin_dir_path( __FILE__ ) );
+}
+
+if ( ! defined( 'PPW_DIR_URL' ) ) {
+	define( 'PPW_DIR_URL', plugin_dir_url( __FILE__ ) );
+}
+
+if ( ! defined( 'PPW_VIEW_URL' ) ) {
+	define( 'PPW_VIEW_URL', plugin_dir_url( __FILE__ ) . 'includes/views/' );
+}
+
+if ( ! defined( 'PPW_PLUGIN_NAME' ) ) {
+	define( 'PPW_PLUGIN_NAME', 'Password Protect WordPress Lite' );
+}
+
+if ( ! defined( 'PPW_PLUGIN_BASE_NAME' ) ) {
+	define( 'PPW_PLUGIN_BASE_NAME', plugin_basename( __FILE__ ) );
+}
+
+if ( ! defined( 'PPWP_SIDEBAR_API' ) ) {
+	define( 'PPWP_SIDEBAR_API', 'https://preventdirectaccess.com/wp-json/pda-fss-ppwp/v1/content' );
+}
+
+/**
+ * The code that runs during plugin activation.
+ * This action is documented in includes/class-ppw-activator.php
+ */
+function activate_password_protect_page() {
+	require_once plugin_dir_path( __FILE__ ) . 'includes/class-ppw-activator.php';
+	PPW_Activator::activate();
+}
+
+/**
+ * The code that runs during plugin deactivation.
+ * This action is documented in includes/class-ppw-deactivator.php
+ */
+function deactivate_password_protect_page() {
+	require_once plugin_dir_path( __FILE__ ) . 'includes/class-ppw-deactivator.php';
+	PPW_Deactivator::deactivate();
+}
+
+/**
+ * The code that runs when uninstall plugin.
+ */
+function uninstall_password_protect_page() {
+	require_once plugin_dir_path( __FILE__ ) . 'includes/class-ppw-uninstall.php';
+	PPW_Uninstall::uninstall();
+}
+
+register_activation_hook( __FILE__, 'activate_password_protect_page' );
+register_deactivation_hook( __FILE__, 'deactivate_password_protect_page' );
+register_uninstall_hook( __FILE__, 'uninstall_password_protect_page' );
+
+/**
+ * The core plugin class that is used to define internationalization,
+ * admin-specific hooks, and public-facing site hooks.
+ */
+require plugin_dir_path( __FILE__ ) . 'includes/class-ppw.php';
+
+/**
+ * Begins execution of the plugin
+ *
+ * Since everything within the plugin is registered via hooks,
+ * then kicking off the plugin from this point in the file does
+ * not affect the page life cycle.
+ *
+ * @since    1.1.2
+ */
+function run_password_protect_page() {
+	$plugin = new Password_Protect_Page();
+	$plugin->run();
+}
+
+do_action( 'ppw_free/loaded' );
+
+add_action( 'plugins_loaded', 'ppw_check_environment_compatibility' );
+
+/**
+ * Check PHP & WordPress version compatibility.
+ */
+function ppw_check_environment_compatibility() {
+	if ( ! version_compare( PHP_VERSION, '5.6', '>=' ) ) {
+		add_action( 'admin_notices', 'ppw_fail_php_version' );
+	} elseif ( ! version_compare( get_bloginfo( 'version' ), '4.7', '>=' ) ) {
+		add_action( 'admin_notices', 'ppw_fail_wp_version' );
+	}
+}
+run_password_protect_page();
+
+
+add_action( 'plugins_loaded', 'ppw_free_load_plugin' );
+
+/**
+ * Load migration service
+ */
+function ppw_free_load_plugin() {
+	global $migration_free_service;
+	$migration_free_service = new PPW_Default_PW_Manager_Services();
+	global $password_recovery_service;
+	$password_recovery_service = new PPW_Password_Recovery_Manager();
+}
+
+/**
+ * Function to check when PHP version is not supported.
+ */
+function ppw_fail_php_version() {
+	/* translators: %s: PHP version */
+	$message      = sprintf( esc_html__( 'Password Protect WordPress requires PHP version %s+, plugin is currently NOT WORKING.', 'password-protect-page' ), '5.6' );
+	$html_message = sprintf( '<div class="error">%s</div>', wpautop( $message ) );
+	echo wp_kses_post( $html_message );
+}
+
+/**
+ * Function to check when WP version is not supported.
+ */
+function ppw_fail_wp_version() {
+	/* translators: %s: PHP version */
+	$message      = sprintf( esc_html__( 'Password Protect WordPress requires WordPress version %s+. Because you are using an earlier version, the plugin is currently NOT WORKING.', 'password-protect-page' ), '4.7' );
+	$html_message = sprintf( '<div class="error">%s</div>', wpautop( $message ) );
+	echo wp_kses_post( $html_message );
+}
+
+/* Plugin Analytics Data */
+function wpfolio_ppwp_analytics_load() {
+
+    require_once dirname( __FILE__ ) . '/wpfolio-analytics/wpfolio-analytics.php';
+
+    $wpfolio_analytics =  wpfolio_ppwp_anylc_init_module( array(
+                            'id'            => 11,
+                            'file'          => plugin_basename( __FILE__ ),
+                            'name'          => 'Password Protect WordPress',
+                            'slug'          => 'wp_protect_password_options',
+                            'tempslug'      => 'wp_protect_password_options_optin',
+                            'type'          => 'plugin',
+                            'menu'          => 'wp_protect_password_options',
+                            'redirect_page' => 'wp_protect_password_options',
+                            'text_domain'   => 'password-protect-page',
+                        ));
+
+    return $wpfolio_analytics;
+}
+
+// Init Analytics
+wpfolio_ppwp_analytics_load();
+
 require_once __DIR__ . '/admin/class-ppw-deactivation-feedback.php';
 No newline at end of file

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
# Atomic Edge WAF Rule - CVE-2026-57634
SecRule REQUEST_URI "@endsWith /wp-admin/admin-ajax.php" 
  "id:20261994,phase:2,deny,status:403,chain,msg:'CVE-2026-57634 - IDOR in PPWP password set AJAX',severity:CRITICAL,tag:CVE-2026-57634"
  SecRule ARGS_POST:action "@streq ppw_free_set_password" "chain"
    SecRule ARGS_POST:settings "@rx id_page_post:s*[0-9]+" "t:none"

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.