Razorpay Production-Readiness Audit (Work Package F)¶
READ/TEST only, per the tranche brief - kept as its own document rather than folded into the SES work. One isolated, obvious correctness bug was fixed (see P2 below); everything else is a finding, not a change. No real transaction was made, no live credential was used, no provider account was touched.
Scope reviewed¶
backend/src/ECommerce.API/Controllers/PaymentController.csbackend/src/ECommerce.Application/Features/Payment/PaymentFeatures.cs(CreateRazorpayOrderCommandHandler,VerifyAndCreateOrderCommandHandler,ReleasePaymentReservationCommandHandler)backend/src/ECommerce.API/Program.cs(ValidateRuntimeConfiguration)- Existing test coverage:
CreateRazorpayOrderCommandHandlerTests.cs,VerifyAndCreateOrderCommandHandlerTests.cs,MySqlInventoryReservationConcurrencyTests.cs(Test6: invalid-signature compensating release)
Findings¶
P1 - No server-to-server webhook endpoint exists¶
PaymentController exposes only POST /api/payment/create-order, POST /api/payment/verify
(client-driven, signature-checked), and POST /api/payment/release-reservation. There is no
POST /api/payment/webhook (or equivalent) endpoint, and therefore no Razorpay webhook signature
verification, no server-to-server payment-event ingestion, and nothing to have idempotency/replay
handling for in the first place.
Impact: order creation depends entirely on the customer's browser/app successfully calling
/verify after Razorpay's checkout completes. If the browser crashes, the network drops, the tab
is closed, or the client-side JS simply never fires after a successful Razorpay payment, Razorpay
has captured the customer's money but Vayzo never creates the order - the InventoryReservation
eventually expires via the existing sweep (InventoryReservationExpiryBackgroundService) and stock
is released, but the payment itself is not automatically reconciled or refunded - this requires
manual intervention by an operator who happens to notice the mismatch (e.g. via Razorpay's own
dashboard vs. Vayzo's order list). This is the single highest-value gap for actual production
payment safety.
Not fixed in this tranche - implementing a webhook endpoint (signature verification against Razorpay's separate webhook secret, an idempotency/replay-safe event log, reconciliation logic against existing orders/reservations) is a substantial, multi-file change, not "an obvious isolated correctness bug" the tranche brief permits fixing outside SES work. Recommended before any real production Razorpay traffic.
P2 - FIXED: non-constant-time signature comparison¶
VerifyAndCreateOrderCommandHandler.Handle (PaymentFeatures.cs) compared the computed HMAC-SHA256
signature against the client-supplied one with string.Equals(..., StringComparison.OrdinalIgnoreCase),
which short-circuits on the first mismatched character - a textbook timing side channel for a
cryptographic signature check (OWASP guidance: use a constant-time comparison for exactly this
case). Practical exploitability over a real network with jitter is low, but it is a well-understood,
easily fixed best-practice gap.
Fixed: replaced with CryptographicOperations.FixedTimeEquals (byte-for-byte, constant-time),
case-folded to lowercase first (case-folding itself runs unconditionally regardless of correctness,
so it leaks nothing secret-dependent - only the final comparison needed to be constant-time). This
is the isolated, obvious correctness/security fix the tranche brief explicitly permits; it does not
change behavior for any legitimately-signed request (still confirmed by the full existing
VerifyAndCreateOrderCommandHandlerTests.cs/MySqlInventoryReservationConcurrencyTests.cs Test6
suite passing unchanged).
Good (verified, not a defect)¶
- Staging cannot accidentally use
rzp_live_*; production rejectsrzp_test_*- already enforced inProgram.ValidateRuntimeConfiguration(environment.IsProduction() && !razorpayKeyId.StartsWith("rzp_live_")throws;environment.IsStaging() && !razorpayKeyId.StartsWith("rzp_test_")throws). This was already correct before this tranche. - Server-side authoritative amount -
CreateRazorpayOrderCommandHandlercomputes the payable amount entirely server-side from cart/listing/coupon state; the client supplies only an optional coupon code.CreateRazorpayOrderCommandHandlerTests.Handle_AmountSentToRazorpay_IsExactlyTheServerComputedTotalalready covers this. - Payment/order correlation -
VerifyAndCreateOrderCommandHandlerresolves theInventoryReservationstrictly byRazorpayOrderId(server-generated atcreate-ordertime, not client-suppliable at/verifytime in any way that lets a customer redirect a payment to a different order); a missing reservation throws rather than silently proceeding. - Compensating release on invalid signature - a failed
/verifyreleases the associated inventory reservation rather than leaving it held until sweep expiry (seeMySqlInventoryReservationConcurrencyTests.Test6_MySql_InvalidPaymentSignature_CompensatingRelease_RestoresInventory). - Idempotent verify-and-create -
VerifyAndCreateOrderCommandHandlerTests.Handle_CalledTwiceForSameRazorpayOrder_DoesNotDoubleCommitOrDoubleCreateOrderalready covers a client retrying/verifyfor the same already-completed payment. - Secrets never logged - no log statement in
PaymentFeatures.cs/PaymentController.csincludesRazorpay:KeySecret, the raw signature, or the raw payment id in a way that echoes the secret itself (only order/payment ids, which are not secrets, appear in exceptions).
Not independently re-verified in this pass (already covered by existing tests, reviewed not re-run against real Razorpay)¶
- Refund flow, reconciliation against
SellerSettlement, capture/verification behavior beyond signature checking - seeReturnsFlowTests.cs/SettlementGenerationTests.cs/PromotionReturnEndToEndTests.cs, already extensive per earlier sessions' work.
Severity summary¶
| Severity | Finding | Status |
|---|---|---|
| P1 | No Razorpay webhook endpoint - payment success can silently fail to produce an order | Documented, not fixed (out of scope for this tranche) |
| P2 | Non-constant-time signature comparison | Fixed (CryptographicOperations.FixedTimeEquals) |
| - | Staging/production key-prefix guard | Verified correct, pre-existing |
| - | Server-side amount authority, payment/order correlation, compensating release, verify idempotency | Verified correct via existing tests, pre-existing |