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.
High because a client-side exchange rate is directly tamper-able via devtools, enabling users to execute currency conversions at fraudulent rates.
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.
// 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() },
});
}
ID: finserv-form-validation.currency-amount.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:
// 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 };
}