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.
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):217-222, type at:70-74azureCalculateRefundat:721-729What
revokeConfirmBody.ExpectedRefundAmountis*float64and 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'sCalculateRefundresponse carries noBillingRefundAmount(in which caseazureCalculateRefundleavescalcRefundAmountnil).When present, the comparison is a bare
math.Abson the numbers.calcRefundCurrencyis 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}/revokewith an empty body (any non-browser client, a retried request whose body was dropped, or a future frontend regression) skips the check, andreturnClient.Postexecutes 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 theMaxPurchaseAmountcurrency gap already recorded for PR LeanerCloud/cloud-commitments-cli#1515.Fix direction
record.Provider == "azure"andExpectedRefundAmountis nil.calcRefundAmountis nil.expected_refund_currencyfield and require an exact match.