A technical audit of a car rental booking service revealed that the Yookassa webhook endpoint accepted payment.succeeded events at face value, allowing forged payment confirmations with a single unauthenticated curl request.
Original vulnerable implementation
The handler in payment_service.py extracted the event type and gateway_payment_id directly from the incoming JSON payload. It performed only one check: whether the gateway_payment_id existed in the local database. If the event type matched payment.succeeded, the payment status was updated and the booking marked as paid. No signature validation, IP filtering, or secondary verification against the Yookassa API was performed.
Because the gateway_payment_id is returned to the client immediately after payment initiation, any user who created a booking could replay the identifier in a forged webhook and complete the transaction without paying.
Why signature checks were not already in place
Yookassa documentation describes an optional Webhook-Signature header using HMAC-SHA256, yet the integration had never configured a webhook_key. The team rejected IP-based allowlisting because traffic passed through reverse proxies, making reliable extraction of the client IP difficult without weakening the trust model. Instead, the fix relies on an authoritative reverse lookup.
Remediation implemented
The corrected handler now treats the incoming webhook solely as a notification trigger. It immediately calls the Yookassa Payments API using the shop ID and secret key to retrieve the current payment state. Status, amount, and currency are taken exclusively from the API response. Mismatches or API failures result in the webhook returning a 5xx status so that Yookassa will retry later. Final statuses are no longer overwritten by subsequent notifications.
A development mode that lacks API credentials retains the original trusting behavior, as no real payments occur in that environment.
Testing and scope of the patch
- test_forged_webhook_ignored_when_gateway_says_pending — forged succeeded event ignored when API reports pending
- test_webhook_amount_mismatch_ignored — status unchanged on amount discrepancy
- test_webhook_confirmed_success_marks_paid — correct transition when API confirms payment
- test_webhook_canceled_marks_failed — cancellation properly reflected
- test_webhook_final_status_not_rewritten — no extra API call for already-final payments
- test_webhook_unknown_payment_ignored — unknown IDs produce no side effects
The PR added 62 lines to the service and 205 lines of tests. Three further improvements—IP allowlisting, signature verification, and removal of double commits—were intentionally left for separate work to keep the security fix minimal and low-risk.


