The Exploit
An unauthenticated attacker can retrieve campaign comments and metadata from any campaign by calling a REST endpoint with no authentication.
GET /wp-json/give-api/v3/campaigns/comments?id=1&perPage=10 HTTP/1.1
Host: target.local
Accept: application/json
The attacker receives a 200 OK response containing the full comment payload for the campaign with ID 1, regardless of whether the campaign is active, published, or marked private. The response includes comment text, author details, timestamps, and internal campaign state—information the site operator never intended to expose to unauthenticated visitors.
What the Patch Did
Before
'permission_callback' => '__return_true',
and
public function get_items($request): WP_REST_Response
{
$campaignId = $request->get_param('id');
$perPage = $request->get_param('perPage');
// ... rest of method without permission checks
}
After
'permission_callback' => '__return_true', // Public endpoint; access is validated inside get_items() based on campaign status and page privacy.
and
public function get_items($request)
{
$campaignId = $request->get_param('id');
$perPage = $request->get_param('perPage');
// ... existing code ...
$canViewPrivate = UserPermissions::campaigns()->canViewPrivate();
if (!$campaign->status->isActive() && !$canViewPrivate) {
return rest_ensure_response(
new WP_Error(
'rest_forbidden',
__('You do not have permission to view this campaign.', 'give'),
['status' => CampaignPermissions::authorizationStatusCode()]
)
);
}
// ... rest of method
}
The patch added a capability check inside the get_items() method that mirrors the permission model: it queries UserPermissions::campaigns()->canViewPrivate() to determine whether the current user (which may be unauthenticated, represented as a user with minimal capabilities) is allowed to view inactive or private campaigns. If the campaign is not active and the user lacks the canViewPrivate permission, the endpoint returns a WP_Error with HTTP 403 Forbidden status. The fix shifts trust from the route-level permission_callback to internal business logic.
Root Cause
CWE-862: Missing Authorization.
The CampaignCommentsController::register_route() method declares 'permission_callback' => '__return_true', a GiveWP pattern that delegates all authorization to the handler function itself. However, the original get_items() method never implemented that internal check. The attacker-controlled request parameter id (campaign ID) flows directly into the comments query without validation of whether the user is allowed to view that campaign's comment thread. The trust boundary—separating unauthenticated visitors from comment readers—is crossed unconditionally. The patch enforces that boundary by checking the campaign's active status and the user's canViewPrivate permission before returning any data.
Why It Works
The load-bearing line is:
if (!$campaign->status->isActive() && !$canViewPrivate)
This condition embodies the authorization rule: "grant access if the campaign is active OR the user has explicit permission to view private campaigns." If you remove it, the function continues to query and return comments for any campaign ID in the database. The surrounding lines—the WP_Error instantiation, the status code helper, the error response—are the defence-in-depth: they signal the denial to the client clearly and allow the site to log the failure. But the conditional itself is irreplaceable; it is the only code that actually evaluates whether the request should succeed.
Hardening Checklist
-
Audit all REST routes with
'permission_callback' => '__return_true': Search your codebase for this pattern and confirm that every matching endpoint implements authorization logic inside its handler method. Usegrepor IDE search across all route registration files. -
Enforce a capability check per resource: For each REST endpoint that exposes user-generated or site-configuration data, call
current_user_can()or your plugin's equivalent permission helper before querying the database. Never rely on the route-level callback alone to gate access. -
Test unauthenticated access: Write automated tests that call each REST endpoint as an unauthenticated visitor and verify the response is 403 Forbidden or 401 Unauthorized, not 200 OK with data. Use tools like
wp-cli rest getor curl to simulate the attack vector. -
Use
rest_ensure_response()withWP_Errorfor denials: When rejecting requests, return a proper REST error object so clients (and logs) can distinguish authorization failures from successful empty responses. -
Validate resource ownership or status before returning: Even if a user has the generic
readcapability, check that they own the resource or that it is published/active before exposing it. Pair capability checks with business-logic validation.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-42642