Skip to content

fix(api): Azure revoke refund-divergence guard is opt-in and currency-blind #96

Description

@cristim

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.

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions