Payment providers require amounts in the smallest currency unit — cents for USD, pence for GBP — but zero-decimal currencies (JPY, KRW, VND, and 13 others) are already expressed in their smallest unit and must not be multiplied by 100. A checkout that applies price * 100 to a JPY price of ¥1,000 submits a charge for ¥100,000 — a 100x overcharge. CWE-681 (Incorrect Conversion between Numeric Types) captures this integer arithmetic error. The ISO 4217 functional correctness requirement means using the wrong currency code (e.g., "JP" instead of "jpy") will also cause the payment to be rejected or processed in the wrong currency. This is a silent production defect: it passes all tests that mock the Stripe API but fails live with real amounts.
Low because zero-decimal currency miscalculation causes 100x overcharges on live transactions for affected currencies, and invalid currency codes cause silent payment failures.
Centralize currency-to-amount conversion in a single utility function that handles zero-decimal currencies correctly.
// lib/currency.ts
const ZERO_DECIMAL = new Set([
'bif', 'clp', 'gnf', 'jpy', 'kmf', 'krw', 'mga',
'pyg', 'rwf', 'ugx', 'vnd', 'vuv', 'xaf', 'xof', 'xpf'
])
export function toProviderAmount(amount: number, currency: string): number {
const code = currency.toLowerCase()
if (!code.match(/^[a-z]{3}$/)) throw new Error(`Invalid ISO 4217 currency code: ${currency}`)
return ZERO_DECIMAL.has(code) ? Math.round(amount) : Math.round(amount * 100)
}
// app/api/payment/route.ts
const paymentIntent = await stripe.paymentIntents.create({
amount: toProviderAmount(priceInLocalUnits, currency),
currency: currency.toLowerCase(), // Stripe requires lowercase ISO 4217
})
Write a unit test for JPY, KRW, and USD to lock in the conversion logic before any multi-currency work ships.
ID: ecommerce-payment-security.payment-errors.correct-currency-codes
Severity: low
What to look for: Enumerate all payment provider API calls that specify a currency parameter. For each, verify the currency code uses the ISO 4217 standard three-letter format (e.g., usd, eur, gbp, jpy). Count total charge/intent creation calls and check each for correct zero-decimal currency handling — Stripe and most providers expect amounts in the smallest currency unit, but zero-decimal currencies (JPY, KRW, VND) should not be multiplied by 100. Check whether the app hardcodes a single currency or dynamically sets it, and whether dynamic currency values are validated.
Pass criteria: All charge requests use valid ISO 4217 currency codes. No more than 0 charge calls should use invalid currency codes or incorrect zero-decimal handling. Zero-decimal currencies are handled correctly (no multiplication by 100). Currency codes match the actual currency the customer is being charged in. Report the count: "X charge calls found, all use valid ISO 4217 codes."
Fail criteria: Invalid currency codes are used, or zero-decimal currencies (JPY, KRW) are incorrectly multiplied by 100, resulting in charges 100x the intended amount.
Skip (N/A) when: The project is a single-currency implementation targeting only USD (or another single major currency) with no dynamic currency selection.
Detail on fail: Describe the error. Example: "stripe.paymentIntents.create({ amount: price * 100, currency: 'JPY' }) multiplies JPY by 100 — a ¥1,000 item would charge ¥100,000 because JPY is a zero-decimal currency"
Remediation: Handle currency unit conversion correctly:
// Zero-decimal currencies should NOT be multiplied by 100
const ZERO_DECIMAL_CURRENCIES = new Set(['bif', 'clp', 'gnf', 'jpy', 'kmf', 'krw', 'mga', 'pyg', 'rwf', 'ugx', 'vnd', 'vuv', 'xaf', 'xof', 'xpf'])
function toProviderAmount(amount: number, currency: string): number {
const code = currency.toLowerCase()
return ZERO_DECIMAL_CURRENCIES.has(code) ? Math.round(amount) : Math.round(amount * 100)
}
const paymentIntent = await stripe.paymentIntents.create({
amount: toProviderAmount(priceInLocalUnits, currency),
currency: currency.toLowerCase(), // Must be lowercase ISO 4217
})