Authenticating a Webhook Isn't Validating It: A Payment-Bypass Lesson (CVE-2026-9189)

작성자

카테고리:

← 피드로
DEV Community · Muni Nitish Kumar Yaddala · 2026-06-18 개발(SW)

Muni Nitish Kumar Yaddala

If your app receives webhooks (Stripe, PayPal, GitHub, a payment IPN, anything), there is a subtle bug class that keeps shipping to production. A recent WordPress CVE is a perfect, minimal teaching example, so let’s use it to make sure none of us write it.

The pattern (this is the part to remember)

Authenticating a webhook  =  "this message really came from the provider"
Validating  a webhook     =  "the data in this message matches what I expect"

Doing the first WITHOUT the second is how money walks out the door.

Enter fullscreen mode Exit fullscreen mode

The real bug, briefly

CVE-2026-9189, in the Contact Form 7 PayPal and Stripe Add-on (version 2.4.9 and older), authenticated PayPal’s IPN correctly (it posted back with cmd=_notify-validate and required VERIFIED), then completed an order using an attacker-controlled invoice value, without checking the amount, currency, or recipient.

The invoice is attacker-controlled, so the attacker does not tamper with a signed message. They make a tiny real payment with the invoice set to a high-value pending order. PayPal genuinely verifies that payment, and the plugin marks the expensive order paid. Unauthenticated. CVSS 5.3, CWE-345.

Attacker pays $1, invoice = order #99 (worth $2,000)
   ->  PayPal sends a GENUINE IPN
   ->  plugin: "is this real?"  ->  VERIFIED   (amount never compared)
   ->  order #99 marked PAID.   $1 for a $2,000 order.

Enter fullscreen mode Exit fullscreen mode

Broken vs. fixed

Broken (authenticity checked, data ignored):

// IPN endpoint open to everyone
function cf7pp_paypal_ipn_auth() {
    return true;
}

// Handler: verifies the message is from PayPal, then trusts the payload
$response = wp_remote_post($paypal_post_url, $args);     // _notify-validate
if (strtolower($response['body']) === 'verified') {
    // attacker controls $data['invoice']; amount never checked:
    cf7pp_complete_payment($data['invoice'], 'completed', $data['txn_id']);
}

Enter fullscreen mode Exit fullscreen mode

Fixed (validate the business data against your stored order):

if (strtolower($response['body']) === 'verified') {
    $order = get_order($data['invoice']);   // load the pending order

    // 1) amount + currency must match what you charged
    if (!hash_equals((string)$order->amount,   (string)$data['mc_gross']) ||
        $order->currency !== $data['mc_currency']) {
        return bail('amount/currency mismatch');
    }
    // 2) the money must have gone to YOU
    if (strcasecmp($order->receiver_email, $data['receiver_email']) !== 0) {
        return bail('wrong recipient');
    }
    // 3) idempotency: ignore replays of an already-processed txn
    if (already_processed($data['txn_id'])) {
        return ok('duplicate ignored');
    }
    complete_payment($order->id, 'completed', $data['txn_id']);
}

Enter fullscreen mode Exit fullscreen mode

The webhook validation checklist

Whenever you handle a payment or webhook callback, do all of these, not just the first:

  • [ ] Authenticate the message (signature, provider postback, shared secret).
  • [ ] Match the amount and currency to the order you created.
  • [ ] Verify the recipient or account is you.
  • [ ] Bind to the order with a server-side value the sender cannot freely set. Do not trust a raw invoice or order_id from the payload as the only link.
  • [ ] Enforce idempotency on the transaction id to defeat replays.
  • [ ] Keep TLS verification ON for any postback (sslverify => true).
  • [ ] Fail closed. If anything does not match, do nothing.

Are you running this plugin?

If you maintain a site using this add-on at 2.4.9 or older to take PayPal payments, update past 2.4.9 now, or disable the PayPal path until you can. Every unpaid order in pending status is a valid target.

Takeaway

The plugin did the hard-looking part (provider authentication) and skipped the easy-looking part (does the money match?). The easy-looking part is the one that protects your revenue. Authenticate the messenger, then always check the message.

Full technical write-up and references: see the canonical post on my blog.
Discovered and responsibly disclosed by Muni Nitish Kumar Yaddala. CVE-2026-9189.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다