Skip to content

Harden ITN handling: validate paid amount and dedupe repeated notifications#16

Open
madsnorgaard wants to merge 1 commit into
Payfast:masterfrom
madsnorgaard:fix/itn-amount-validation-and-idempotency
Open

Harden ITN handling: validate paid amount and dedupe repeated notifications#16
madsnorgaard wants to merge 1 commit into
Payfast:masterfrom
madsnorgaard:fix/itn-amount-validation-and-idempotency

Conversation

@madsnorgaard

Copy link
Copy Markdown

Two payment-integrity defects in `onNotify()` / `processOrder()`, independent of #12.

1. Amount tampering (security)

The ITN signature is verified, but `amount_gross` originates from the process-page POST the buyer controls. The handler recorded it verbatim, so a completed payment could be booked for less than the cart total. The paid gross is now compared to `$order->getTotalPrice()` (ZAR, 0.01 tolerance); mismatches are logged and rejected. This mirrors the amount check in PayFast's own ITN integration guidance.

2. Duplicate payments (idempotency)

PayFast re-sends the ITN until it receives a 200, so each retry created another completed payment for the same pf_payment_id. processOrder() now returns early when a payment with that remote_id already exists.

Supporting fix

loadPaymentByRemoteId() queried order_id (shared by every payment on the order) despite its name; it now queries remote_id (the PayFast transaction id) and returns FALSE safely if payment storage cannot load.

Scope: one file, OffsiteRedirect.php. No behaviour change for correctly-formed single ITNs.

Two defects in onNotify()/processOrder() that affect payment integrity:

1. Amount tampering. The ITN signature is verified, but the gross amount
   originates from the process-page POST the buyer controls. The handler
   recorded amount_gross verbatim without comparing it to the order total,
   so a completed payment could be booked for less than the cart value.
   Now the paid gross is compared to $order->getTotalPrice() (ZAR, 0.01
   tolerance) and mismatches are logged and rejected.

2. Duplicate payments. Payfast re-sends the ITN until it receives a 200,
   so each retry created another completed payment for the same
   pf_payment_id. processOrder() now short-circuits when a payment with
   that remote_id already exists.

loadPaymentByRemoteId() is corrected to query remote_id (the Payfast
transaction id) instead of order_id, which every payment on the order
shares, and to bail out safely if the payment storage cannot load.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant