# Currency conversions server-side, locked at transaction time

- **Pattern:** `ab-001440` (`finserv-form-validation.currency-amount.server-side-conversion`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001440
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001440)

## Why it matters

A client-side exchange rate is fetched at page load and may be stale seconds later. More critically, it can be tampered: any user with browser devtools can edit the rate in memory before submitting, receiving a conversion at an artificially favorable rate. OWASP A03 (Injection) and CWE-602 (Client-Side Enforcement of Server-Side Security) both flag trusting client-calculated financial values. PCI DSS Req-6.2.4 requires server-side validation of all input data used in financial operations.

## Severity rationale

High because a client-side exchange rate is directly tamper-able via devtools, enabling users to execute currency conversions at fraudulent rates.

## Remediation

Move all currency conversion logic into the server route and persist the locked rate with the transaction record. Never accept a converted amount or exchange rate from the client request body.

```ts
// src/app/api/transfers/route.ts
export async function POST(req: Request) {
  const { fromAmountCents, fromCurrency, toCurrency } = await req.json();

  // Fetch and lock the rate server-side
  const rate = await getExchangeRate(fromCurrency, toCurrency);
  const toAmountCents = Math.round(fromAmountCents * rate);

  await db.transaction.create({
    data: { fromAmountCents, toAmountCents, exchangeRate: rate, lockedAt: new Date() },
  });
}
```

## Detection

- **ID:** `server-side-conversion`
- **Severity:** `high`
- **What to look for:** Count all currency conversion call sites in the codebase. For each, classify whether the conversion happens client-side (in `src/components/`, `src/app/` page files, or browser JS) or server-side (in `src/app/api/`, `pages/api/`, or backend services). Quote the conversion expression found (e.g., `amount * exchangeRate` and where it lives). Check whether the exchange rate is persisted alongside the transaction with a `lockedAt` timestamp. Report: "X of Y conversion sites are server-side with locked rates."
- **Pass criteria:** At least 100% of currency conversions happen server-side. The conversion rate is locked in at transaction time with a stored timestamp. No more than 0 conversions use a client-fetched rate for the final transaction amount. Report even on pass: "X conversion sites found, all server-side with locked rates."
- **Fail criteria:** Any conversion uses a client-side rate for the committed transaction amount, or the rate is not stored alongside the transaction record.
- **Do not pass when:** The server fetches the rate but does not persist it with the transaction — rate auditability requires the locked rate to be stored.
- **Skip (N/A) when:** The project does not support multi-currency transactions (0 conversion call sites found after searching for exchange rate, currency conversion, or FX logic).
- **Detail on fail:** Identify the conversion timing. Example: `"Currency conversion happens in client-side JavaScript. Exchange rate fetched on page load and may be stale by time of submission."` or `"Server converts at request time but conversion rate is fetched from external API without locking — rate could change between fetch and transaction commit"`
- **Cross-reference:** The API Security audit covers request tampering and parameter injection that complement server-side conversion enforcement.
- **Remediation:** Client-side conversions are unreliable and enable fraud. In `src/app/api/transfers/route.ts` or the relevant server route:

  **Server-side conversion pattern in `src/app/api/transfers/route.ts`:**
  ```ts
  // API route: POST /api/transfer
  export async function POST(req) {
    const { fromAmount, fromCurrency, toCurrency } = req.body;

    // Fetch latest rate (or use locked rate from DB)
    const rate = await getExchangeRate(fromCurrency, toCurrency);

    // Convert server-side
    const toAmount = Math.round(fromAmount * 100 * rate) / 100; // cents

    // Commit to DB with locked rate and amount
    const transaction = await db.transactions.create({
      fromAmount: fromAmount * 100, // as cents
      toCurrency,
      toAmount: toAmount * 100, // as cents
      exchangeRate: rate,
      lockedAt: new Date(), // Lock rate at this moment
    });

    return { success: true, transaction };
  }
  ```

## External references

- cwe CWE-20
- owasp A03
- cwe CWE-602
- pci-dss Req-6.2.4

Taxons: injection-and-input-trust, data-integrity

HTML version: https://auditbuffet.com/patterns/ab-001440
