You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix(api): Azure revoke refund-divergence guard is opt-in and currency-blind #96
Reviewed commit: be11bdcb5 (origin/main), from the 2026-07-28 full-repo review.
Severity: HIGH. An irreversible refund can execute at an amount materially different from the one the user consented to.
Where
internal/api/handler_purchases_revoke.go:657-664 (the TOCTOU guard)
body parsed at :217-222, type at :70-74
azureCalculateRefund at :721-729
What
revokeConfirmBody.ExpectedRefundAmount is *float64 and its doc says "Required when the purchase has an Azure revocation window", but nothing enforces that.
The TOCTOU check is if expectedRefundAmount != nil && calcRefundAmount != nil { ... }. It is skipped entirely when the client omits the field, and equally skipped when Azure's CalculateRefund response carries no BillingRefundAmount (in which case azureCalculateRefund leaves calcRefundAmount nil).
When present, the comparison is a bare math.Abs on the numbers. calcRefundCurrency is read but only used in the error message, never compared against anything.
Failure scenario (a): guard skipped
The frontend quote step shows the user EUR 4,200. The user confirms; a request that reaches POST /api/purchases/{id}/revoke with an empty body (any non-browser client, a retried request whose body was dropped, or a future frontend regression) skips the check, and returnClient.Post executes the return at whatever Azure now quotes, for example EUR 1,100 after a fee-tier boundary was crossed. The user is refunded EUR 3,100 less than they consented to, irreversibly.
Failure scenario (b): currency-blind comparison
Quote returns {amount: 4200, currency: "EUR"}. By confirm time Azure quotes {amount: 4200.00, currency: "USD"}. |4200-4200| = 0 < 0.01, the guard passes, and a materially different refund is accepted. Same shape as the MaxPurchaseAmount currency gap already recorded for PR LeanerCloud/cloud-commitments-cli#1515.
Fix direction
Reject with 400 when record.Provider == "azure" and ExpectedRefundAmount is nil.
Fail closed (422) when calcRefundAmount is nil.
Add an expected_refund_currency field and require an exact match.
Activity