The Exploit
An unauthenticated attacker with network access to the WordPress REST API can invoke the /wp-json/payu/v1/get-shipping-cost endpoint to bypass authentication entirely. The endpoint accepts arbitrary email and transaction ID parameters without verifying the caller's identity, then processes those values in downstream code that modifies user state.
POST /wp-json/payu/v1/get-shipping-cost HTTP/1.1
Host: target.com
Content-Type: application/json
{
"email": "[email protected]",
"txnid": "12345_order",
"udf4": "session_key_value",
"address": "123 Main St"
}
The response confirms successful processing without requiring authentication. An attacker observes a 200 status code with shipping cost data, and if the request parameters align with an existing order or guest session, the endpoint will extract and process user-controlled data within a protected context. The vulnerability lies upstream: no permission check guards the endpoint before the callback executes, allowing unauthenticated callers to reach code paths that validate only an optional auth token — not membership in a privileged role.
What the Patch Did
Before:
public function getPaymentFailedUpdate()
{
register_rest_route('payu/v1', '/get-shipping-cost', array(
'methods' => ['POST'],
'callback' => array($this, 'payuShippingCostCallback'),
'permission_callback' => '__return_true'
));
}
After:
public function getPaymentFailedUpdate()
{
register_rest_route('payu/v1', '/get-shipping-cost', array(
'methods' => ['POST'],
'callback' => array($this, 'payuShippingCostCallback'),
'permission_callback' => function() {
return current_user_can('manage_woocommerce') || $this->payu_validate_authentication_token(...);
}
));
}
The patch replaced 'permission_callback' => '__return_true' — which unconditionally permits all requests — with a closure that enforces either a WordPress capability check via current_user_can('manage_woocommerce') or validation of a cryptographic authentication token through payu_validate_authentication_token(). This is a guard-rail control that executes before the callback function itself, at the REST API dispatcher level.
Additionally, the patch added explicit null-checks for required parameters:
if (!isset($parameters['email']) || !isset($parameters['txnid'])) {
return new WP_REST_REST_Response([
'status' => 'false',
'data' => [],
'message' => 'Missing required parameters'
], 400);
}
And validation of the auth token header:
$token = isset($auth['Auth-Token']) ? $auth['Auth-Token'] : '';
if (empty($token)) {
return new WP_REST_Response([
'status' => 'false',
'data' => [],
'message' => 'Token is missing'
], 401);
}
Root Cause
CWE-287 (Improper Authentication) and CWE-20 (Improper Input Validation) chain to create unauthenticated access to a sensitive endpoint. The attacker-controlled parameters email, txnid, udf4, and address are passed in the POST body and reach the callback function without any permission boundary. The REST dispatcher evaluates permission_callback: __return_true, which is a magic function that always returns true, effectively disabling authentication. No capability check (e.g., current_user_can()) guards entry. Inside the callback, the code sanitizes but does not validate presence of required parameters before use, and attempts to read $auth['Auth-Token'] without verifying the key exists. The trust boundary — the line between unauthenticated and authenticated callers — is never enforced because the permission callback is a no-op.
Why It Works
The load-bearing line is the new permission_callback closure that invokes current_user_can('manage_woocommerce') or payu_validate_authentication_token(). Without this change, the endpoint remains callable by any HTTP client regardless of session or token state. The patch's secondary defensive layers — null-checks on $parameters['email'] and $parameters['txnid'], and the isset-guard on $auth['Auth-Token'] — prevent downstream undefined-key warnings and ensure that if the permission callback is somehow bypassed, the callback itself will reject malformed requests early. However, those checks are redundant defenses; the permission callback is the critical control. If you removed it and restored 'permission_callback' => '__return_true', the vulnerability would re-emerge immediately because the code path would be reachable by unauthenticated users. The secondary guards catch sloppy input, but they do not authenticate the caller — only the permission callback does.
Hardening Checklist
- Audit all REST endpoints for
'permission_callback' => '__return_true'or missing permission_callback entirely. Usewp-cli plugin code-searchor grep to identify register_rest_route() calls without a real permission_callback function. - Implement mandatory authentication at the route level, not inside the callback. Use
current_user_can()in a closure or a dedicated function that returns a WP_Error on failure. Never delay authentication to callback logic. - Validate required parameters before use. Check isset() on all expected keys in
$request->get_params()or$request->get_json_params()before sanitization, and return a 400 WP_REST_Response if any are missing. - Use
$request->get_header()instead ofapache_request_headers()to read headers in a portable, testable way. Always check isset() or truthiness before dereferencing array keys. - Test unauthenticated access to all endpoints. Write a PHPUnit test that invokes each REST endpoint as an unauthenticated user and asserts a 401 or 403 response. Add the test to CI/CD.
References
- https://nvd.nist.gov/vuln/detail/CVE-2024-12264