fix(security): unique payment reference_id, reject replayed callbacks (H3)
reference_id (the gateway's settled-transaction ref) was not unique, so the same successful callback — or a RefNum replayed onto another order — could credit twice. Add a unique index (NULL until success, so pending/failed rows don't collide) and an application-level pre-check in the callback that fails the payment if the reference already belongs to another order. The unique index is the hard backstop behind the check. Regression: PaymentCallbackAmountTest::testReplayedGatewayReferenceIsRejected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -286,6 +286,19 @@ class PaymentController extends BaseController
|
||||
return $this->redirectToFrontend($payment, false);
|
||||
}
|
||||
|
||||
// A gateway reference identifies exactly one settled transaction. If it
|
||||
// already belongs to another payment, this is a replay — reject it. The
|
||||
// unique DB index on reference_id is the hard backstop behind this check.
|
||||
if ($result->referenceId !== '') {
|
||||
$owner = $this->paymentRepo->findByReferenceId($result->referenceId);
|
||||
if ($owner !== null && $owner->getId() !== $payment->getId()) {
|
||||
$payment->setStatus(Payment::STATUS_FAILED);
|
||||
$this->paymentRepo->save($payment);
|
||||
|
||||
return $this->redirectToFrontend($payment, false);
|
||||
}
|
||||
}
|
||||
|
||||
$payment->setStatus(Payment::STATUS_SUCCESS);
|
||||
$payment->setReferenceId($result->referenceId);
|
||||
$this->paymentRepo->save($payment);
|
||||
|
||||
Reference in New Issue
Block a user