The Exploit
An authenticated WordPress user with subscriber-level permissions or higher can modify the default payment method setting for the entire RegistrationMagic plugin without holding administrative privileges.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target-wordpress.local
Content-Type: application/x-www-form-urlencoded
Cookie: wordpress_logged_in=<subscriber_session>
action=rm_options_default_payment_method&rm_sec_nonce=<valid_nonce>&payment_method=paypal_express
The server responds with a 200 status and no error message. The attacker then navigates to the RegistrationMagic settings page (or queries the database) and observes that the default payment method has been changed from its original value (e.g., stripe) to paypal_express. All new user registrations will now attempt to charge via PayPal instead, allowing the attacker to redirect payments or disrupt the site's payment flow.
What the Patch Did
Before
if(check_ajax_referer('rm_ajax_secure','rm_sec_nonce')) {
After
if(check_ajax_referer('rm_ajax_secure','rm_sec_nonce') && (current_user_can('manage_options') || current_user_can('rm_options_managemanage_options'))) {
The patch adds a capability check using WordPress's current_user_can() function. The original code verified only the AJAX nonce (rm_sec_nonce), which proves the request originated from the same site and was not cross-site forged — but proves nothing about who is making the request. The fix enforces that the calling user must possess either the manage_options capability (held only by administrators) or the plugin-specific rm_options_managemanage_options capability (likely reserved for payment settings managers). Unauthenticated users and low-privilege subscribers now receive an implicit false from the conditional and cannot reach the payment method update logic.
Root Cause
CWE-862: Missing Authorization
The AJAX handler rm_options_default_payment_method() on line 1223 of includes/class_registration_magic.php receives the payment_method parameter from the POST request body. The function trusts that check_ajax_referer() is a sufficient guard: the nonce proves the request came from the authenticated WordPress session, not a cross-site attacker. However, nonce validation does not verify the caller's role or permissions. Any user who successfully logged in — including a subscriber with no administrative duties — can forge a valid nonce-protected request and reach the sink where the payment method is updated. The trust boundary between "authenticated" and "authorized" was collapsed: the developer conflated CSRF prevention with access control.
Why It Works
The load-bearing line is current_user_can('manage_options'). If this check were removed and only the nonce remained, the vulnerability would persist unchanged. The second capability check, current_user_can('rm_options_managemanage_options'), exists for defence-in-depth: it allows the plugin to grant payment-settings access to custom roles without requiring full administrator status. A developer who removes only the second check still gains protection from the first. The logical AND operator (&&) between nonce and capability ensures both controls must pass; removing either one re-opens the vulnerability. The nonce check is necessary to prevent CSRF attacks against administrators, but it is not sufficient for authorization — hence the engineer correctly added the capability layer afterward, making the fix resilient to partial removal or misunderstanding.
Hardening Checklist
-
Audit every AJAX handler for capability checks: Search
admin-ajax.phphandlers and AJAX actions forcheck_ajax_referer()calls that lack a subsequentcurrent_user_can()gate. Usewp_verify_nonce()orcheck_ajax_referer()for CSRF only; never assume nonce validation implies authorization. -
Require explicit capability definition in plugin documentation: When a function modifies site settings, declare the minimum required capability (
manage_options, custom caps, etc.) in inline code comments and user-facing docs. This prevents future maintainers from accidentally weakening the check. -
Use
wp_enqueue_script()withwp_localize_script()to inject nonces: Nonces passed via hidden form fields or script attributes are easier to audit than hardcoded strings. Centralize nonce generation so a grep forwp_create_nonce()reveals all security-sensitive endpoints. -
Implement unit tests that assert authorization: For every settings-mutation action, write a test that verifies a subscriber role gets a
403or auth error, and an admin role succeeds. Use WordPress testing frameworks (WP_UnitTestCase) or snapshot tests against mock user roles. -
Enable WordPress security.txt or plugin security headers: Publish a security.txt or SECURITY.md file directing researchers to responsible disclosure. Many vulnerabilities in widely-used plugins remain unpatched for months because the vendor has no public security contact.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-32385