IBAN and US ABA account numbers have structurally incompatible formats — IBANs are 15-34 alphanumeric characters with a country prefix; US account numbers are 8-17 digits. A single generic regex that accepts either format will reject valid inputs from one region and accept invalid inputs from the other. OWASP A03 and PCI DSS Req-6.2.4 require input validation appropriate to the data type. Misrouted payments due to format confusion create irreversible ACH or SWIFT transactions, customer losses, and regulatory liability.
High because accepting incorrect account number formats routes payments to invalid destinations, causing irreversible transaction failures and potential fund loss.
Use a discriminated union in src/lib/schemas.ts so each region's account number is validated against its own format rules, and route server-side validation through the same schema.
// src/lib/schemas.ts
const accountSchema = z.discriminatedUnion('region', [
z.object({
region: z.literal('EU'),
accountNumber: z.string().refine(validateIBAN, 'Invalid IBAN'),
}),
z.object({
region: z.literal('US'),
accountNumber: z.string().regex(/^\d{8,17}$/, 'US account must be 8-17 digits'),
}),
]);
Apply this schema in src/app/api/transfers/route.ts and mirror the same constraints in the client form component in src/components/.
ID: finserv-form-validation.account-routing.account-format-validation
Severity: high
What to look for: Count all account number input fields across the project. For each, classify whether validation is region-aware (IBAN structure for EU, ABA length for US) or generic (accepts any digit string). Quote the validation regex or function found. Check whether validation runs server-side. Report: "X of Y account inputs have region-specific server-side validation."
Pass criteria: At least 100% of account number inputs have region-specific format validation that runs server-side. IBAN inputs validate the 2-letter country prefix + check digits. US account inputs validate minimum 8 and maximum 17 digits. Report even on pass: "X account fields found with region-specific validation."
Fail criteria: Any account input accepts generic digit strings without region-specific format checking, or validation is only client-side.
Do not pass when: A single generic regex like /^\d{10,20}$/ handles all regions — this accepts invalid IBANs and rejects valid short US accounts.
Skip (N/A) when: The project does not accept account numbers (0 account input fields found), or only supports a single region with simple numeric IDs.
Detail on fail: Example: "Account validation accepts any 10-20 digit string. No distinction between IBAN (24-34 alphanumeric) and US ABA account (8-17 digits)." or "Only client-side regex validation; no server-side IBAN structure check"
Cross-reference: The iban-structure-validation check in this category verifies IBAN checksum specifically; this check covers the broader format routing.
Remediation: Account format varies by country. Add region-specific validators in src/lib/validators.ts:
IBAN validation in src/lib/validators.ts:
function validateIBAN(iban) {
iban = iban.replace(/\s/g, '').toUpperCase();
if (!/^[A-Z]{2}\d{2}[A-Z0-9]{1,30}$/.test(iban)) return false;
const rearranged = iban.slice(4) + iban.slice(0, 4);
const digits = rearranged.replace(/[A-Z]/g, c => (c.charCodeAt(0) - 55).toString());
let remainder = digits;
while (remainder.length > 2) {
remainder = ((BigInt(remainder.slice(0, 9)) % 97n).toString() + remainder.slice(9));
}
return remainder % 97n === 1n;
}
Zod schema in src/lib/schemas.ts:
const accountSchema = z.discriminatedUnion('region', [
z.object({
region: z.literal('EU'),
account: z.string().refine(validateIBAN, 'Invalid IBAN'),
}),
z.object({
region: z.literal('US'),
account: z.string().regex(/^\d{8,17}$/, 'Invalid US account number'),
}),
]);