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
The Azure RI exchange execute endpoint (POST /api/ri-exchange/azure-instances/exchange) has no submit-time idempotency. A client retry after a timeout can commit the same exchange twice, spending roughly twice the intended amount while each individual request stays inside MaxPurchaseAmount.
ExecuteExchange's doc comment presents Azure's CalculateExchange session ID as the replay-protection mechanism. It is not one as used here: executeAzureExchange deliberately mints a fresh session on every request (internal/api/handler_ri_exchange.go, the client.CalculateExchange(...) re-quote before commit) precisely so a client-supplied or stale session can never bypass the guardrails. The consequence is that two POSTs of the same logical exchange carry two different session IDs, so Azure's own replay protection never fires. There is no request-level dedupe either, unlike the purchase path's purchaseIdempotencyKey (internal/api/handler_purchases.go, refs #644 / #643).
Concrete scenario
Source reservation has quantity 10. Caller POSTs an exchange of 4 of them.
The Azure LRO is polled with PollUntilDone and takes minutes.
The client times out and retries the identical request.
The second request re-quotes against the now-quantity-6 reservation, passes every guardrail (ownership, region, currency, cap), and commits a second exchange of 4 plus a second target purchase.
Net effect: twice the intended spend, each half individually under the cap.
Compounding factor: a ctx cancellation mid-ExecuteExchange is mapped to a flat 500 "exchange execution failed" (mapAzureExchangeError), even though BeginPost may already have submitted the operation. That is exactly the response that provokes the retry in step 3.
Fix direction
Fingerprint the request over (creator, subscription_id, sorted sources incl. quantity, sorted targets, cap, currency) and dedupe on it, mirroring purchaseIdempotencyKey.
A post-submit cancellation should surface as "submitted, outcome unknown - verify in the portal", not a flat failure that invites a retry.
Scope note
The AWS executeExchange handler in the same file has the same absence, so this is a pattern gap across both providers, not a regression introduced by #1515 / PR for issue #596. Fixing it should cover both paths.
Found during adversarial review of PR #1515 (Azure RI exchange execute). Out of scope for that PR, which fixed the Regions-constraint source gap.
Summary
The Azure RI exchange execute endpoint (
POST /api/ri-exchange/azure-instances/exchange) has no submit-time idempotency. A client retry after a timeout can commit the same exchange twice, spending roughly twice the intended amount while each individual request stays insideMaxPurchaseAmount.ExecuteExchange's doc comment presents Azure'sCalculateExchangesession ID as the replay-protection mechanism. It is not one as used here:executeAzureExchangedeliberately mints a fresh session on every request (internal/api/handler_ri_exchange.go, theclient.CalculateExchange(...)re-quote before commit) precisely so a client-supplied or stale session can never bypass the guardrails. The consequence is that two POSTs of the same logical exchange carry two different session IDs, so Azure's own replay protection never fires. There is no request-level dedupe either, unlike the purchase path'spurchaseIdempotencyKey(internal/api/handler_purchases.go, refs #644 / #643).Concrete scenario
PollUntilDoneand takes minutes.Compounding factor: a
ctxcancellation mid-ExecuteExchangeis mapped to a flat 500 "exchange execution failed" (mapAzureExchangeError), even thoughBeginPostmay already have submitted the operation. That is exactly the response that provokes the retry in step 3.Fix direction
(creator, subscription_id, sorted sources incl. quantity, sorted targets, cap, currency)and dedupe on it, mirroringpurchaseIdempotencyKey.subscription_id:ListExchangeableReservationsis tenant-wide, so a scope-blind token can alias across subscriptions.Scope note
The AWS
executeExchangehandler in the same file has the same absence, so this is a pattern gap across both providers, not a regression introduced by #1515 / PR for issue #596. Fixing it should cover both paths.Found during adversarial review of PR #1515 (Azure RI exchange execute). Out of scope for that PR, which fixed the Regions-constraint source gap.