Payment Bugs

Webhook Accepted Without Signature Verification

The payment webhook endpoint POST /api/webhooks/payment processes any incoming request without verifying the provider's HMAC-SHA256 signature. A forged webhook event claiming a payment succeeded for order 701 can be sent by any HTTP client and will be processed as a legitimate payment, crediting an order without a real charge having occurred.

HighIntermediateSecurity testingAPI testingManual testing

// UNDERSTAND

// Symptoms

  • A POST to /api/webhooks/payment with a forged payment.succeeded body and no Stripe-Signature header transitions order 701 from 'pending' to 'paid'
  • The webhook endpoint returns 200 OK for requests that include an invalid or missing signature header
  • An order can be marked as paid without a corresponding charge appearing in the payment provider's test dashboard
  • The application logs show webhook events being processed without any signature verification step

// Root Cause

  • The webhook handler does not read the provider's signature header (e.g. Stripe-Signature) and does not compute and compare the expected HMAC-SHA256 from the raw request body and the webhook signing secret. Every POST to the endpoint is processed as a genuine provider event.
  • The webhook signing secret is either not configured in the application's environment variables or the signature verification code path exists but is never invoked — the webhook handler was implemented in a development environment where verification was temporarily disabled and never re-enabled.

// Where It Appears

  • Payment webhook endpoints in any application that integrates with Stripe, PayPal, or similar providers
  • Subscription billing webhook handlers that process refund, cancellation, and dispute events
  • Applications where the webhook endpoint was added quickly and signature verification was deferred as a 'later' task
  • Environments where the webhook secret was set in production but not in staging, leaving staging unprotected

// REPRODUCE & TEST

// How to Reproduce

  1. 01Find a test order with ID 701 and confirm its status is 'pending' via GET /api/orders/701
  2. 02Construct a forged webhook body: { "type": "payment.succeeded", "data": { "object": { "metadata": { "orderId": "701" } } } }
  3. 03Send POST /api/webhooks/payment with the forged body and the Content-Type: application/json header — include no Stripe-Signature header or use a fabricated value such as 'fake-signature'
  4. 04Send GET /api/orders/701 and read the status field
  5. 05If the status changed from 'pending' to 'paid', the forged event was accepted without signature verification

// Test Data Needed

  • A test order 701 in 'pending' status
  • A way to send a raw HTTP POST request with a controlled body and headers (Postman or curl)

// Manual Testing Ideas

  • Send the forged payment.succeeded event with no signature header and observe whether the order status changes
  • Send the event with a Stripe-Signature header set to a random string (e.g. 'invalid-sig') and observe whether the application still processes it
  • Compare with the provider's legitimate webhook: generate a correctly signed event using the Stripe CLI (stripe trigger payment_intent.succeeded) and confirm that it is also accepted — this confirms the endpoint works for real events while also verifying the bug for fake ones
  • Check the application logs: does a signature verification step appear in the log before the order update? Its absence confirms no verification is performed
  • Test whether the endpoint returns a different status code for missing vs. invalid vs. valid signatures — a production-hardened endpoint should return 400 for invalid signatures

// API Testing Ideas

  • Confirm GET /api/orders/701 returns status: 'pending'
  • Send POST /api/webhooks/payment with forged body { "type": "payment.succeeded", "data": { "object": { "metadata": { "orderId": "701" } } } } and no Stripe-Signature header
  • Assert the response is 400 Bad Request — a correctly implemented endpoint must reject requests with missing or invalid signatures
  • If the response is 200, send GET /api/orders/701 and confirm whether the status changed to 'paid' — a status change confirms the forged event was fully processed

// Automation Idea

Send POST /api/webhooks/payment with a forged payment.succeeded body for order 701 and no Stripe-Signature header. Assert the HTTP response is 400 Bad Request. Then send GET /api/orders/701 and assert the status remains 'pending'. If the webhook endpoint returns 200 and the order status changes to 'paid', the signature verification is absent and the test fails.

// Expected Result

POST /api/webhooks/payment rejects any request without a valid Stripe-Signature header with HTTP 400 Bad Request, and the order status is not changed.

// Actual Result (Example)

POST /api/webhooks/payment with a forged payment.succeeded body and no Stripe-Signature header returns 200 OK. GET /api/orders/701 returns { "status": "paid" } — the forged event was processed and the order was credited without a real charge.

// REPORT IT

Example Bug Report

Title
Forged payment.succeeded webhook transitions order 701 to 'paid' with no signature header
Severity
High
Environment
Staging environment Postman Test order 701 in 'pending' status Endpoint: POST /api/webhooks/payment
Steps to Reproduce
  1. 01Confirm GET /api/orders/701 returns status: 'pending'
  2. 02Send POST /api/webhooks/payment with body { "type": "payment.succeeded", "data": { "object": { "metadata": { "orderId": "701" } } } } and no Stripe-Signature header
  3. 03Observe the HTTP response status code
  4. 04Send GET /api/orders/701 and read the status field
Expected Result
The webhook endpoint returns 400 Bad Request and the order status remains 'pending'.
Actual Result
The webhook endpoint returns 200 OK and GET /api/orders/701 returns status: 'paid'. The forged event was processed without any signature check.
Impact
An attacker can mark any order as paid without making a real payment, fraudulently obtaining goods or services. Refund events can also be forged to trigger bogus refunds, causing financial loss.

// RELATED