Skip to content

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.cs
  • backend/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 rejects rzp_test_* - already enforced in Program.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 - CreateRazorpayOrderCommandHandler computes the payable amount entirely server-side from cart/listing/coupon state; the client supplies only an optional coupon code. CreateRazorpayOrderCommandHandlerTests.Handle_AmountSentToRazorpay_IsExactlyTheServerComputedTotal already covers this.
  • Payment/order correlation - VerifyAndCreateOrderCommandHandler resolves the InventoryReservation strictly by RazorpayOrderId (server-generated at create-order time, not client-suppliable at /verify time 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 /verify releases the associated inventory reservation rather than leaving it held until sweep expiry (see MySqlInventoryReservationConcurrencyTests.Test6_MySql_InvalidPaymentSignature_CompensatingRelease_RestoresInventory).
  • Idempotent verify-and-create - VerifyAndCreateOrderCommandHandlerTests.Handle_CalledTwiceForSameRazorpayOrder_DoesNotDoubleCommitOrDoubleCreateOrder already covers a client retrying /verify for the same already-completed payment.
  • Secrets never logged - no log statement in PaymentFeatures.cs/PaymentController.cs includes Razorpay: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 - see ReturnsFlowTests.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