Skip to content

fix(exchange): RI-exchange spend caps are compared against a quote whose CurrencyCode is never checked #42

Description

@cristim

Reviewed commit: be11bdcb5 (origin/main), from the 2026-07-28 full-repo review.

Severity: HIGH. A USD-named spend cap is enforced against a number that may not be dollars.

Where

  • pkg/exchange/exchange.go:249-254 (captures CurrencyCode, never used)
  • pkg/exchange/exchange.go:312-321 (checkInitialQuote), :326-339 (checkReQuote)
  • pkg/exchange/auto.go:318-325 and :505-535 (compare against MaxPaymentPerExchangeUSD / MaxPaymentDailyUSD)
  • internal/config/store_postgres.go:2642 (GetRIExchangeDailySpend)

What

getQuoteWithAPI reads out.CurrencyCode into the summary and then every guardrail compares PaymentDue (a bare decimal parsed from out.PaymentDue) against a USD-named cap, with no assertion that the quote is USD-denominated. The ledger likewise SUMs payment_due with no currency column.

Failure scenario

An account whose EC2 exchange quotes come back in a non-USD currency returns PaymentDue="900", CurrencyCode="JPY".

checkInitialQuote compares 900 against MaxPaymentDueUSD = 1000, passes, and AcceptReservedInstancesExchangeQuote fires. The operator's stated $1000/day ceiling is enforced against a number that is not dollars, and the error runs in either direction: silently blocking a legal exchange, or silently authorizing one an order of magnitude over the cap.

Fix direction

Fail closed in checkInitialQuote / checkReQuote when q.CurrencyCode != "USD", and record the currency on the ledger row, rather than comparing across denominations.

Related

Exchange-side sibling of the MaxPurchaseAmount currency rule established in PR LeanerCloud/cloud-commitments-cli#1515. LeanerCloud/cloud-commitments-cli#1087 covers the AWS Savings Plans purchase path being USD-only, a different call site.

Findings from the 2026-09-02 codebase audit

Added by an automated audit of 3c0f8ac94048a2c36fce5ccddee54e6c4849a5cd (tip of origin/main). Each item below was reported by one reviewer and independently confirmed by a second that did not write it. Full report: docs/audits/codebase-audit-2026-09-02.md.

A09-031 (medium)

Coverage note for the fix, from reading all six test files in pkg/exchange rather than grepping. Nothing in the package drives checkInitialQuote / checkReQuote with a CurrencyCode other than USD, so the cap comparison this issue is about has no test that could fail. Two sibling guards on the same irreversible path are equally unverified: no case sets PaymentDueRaw to the empty string on a quote destined for those checks, and assertAccount / ExpectedAccount appear in no test file at all. exchange_test.go holds only TestParseDecimalRat, TestPaymentDueUSDStr_InJSON and TestSpendCapComparison, the last exercising big.Rat.Cmp directly rather than the guard; fail_loud_test.go's seqQuoteOut always sets a non-empty PaymentDue (:47-52) and never sets currency or expected account. Three table cases -- an empty PaymentDue, a EUR quote, and an account mismatch, each asserting AcceptReservedInstancesExchangeQuote was never called -- would give this fix teeth. (The EUR/empty OfferingOption.CurrencyCode cases in reshape_crossfamily_test.go exercise passesDollarUnitsCheck, not the cap.) (audit finding A09-031)

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