# Account number format validated per region (IBAN/ABA)

- **Pattern:** `ab-001443` (`finserv-form-validation.account-routing.account-format-validation`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001443
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001443)

## Why it matters

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.

## Severity rationale

High because accepting incorrect account number formats routes payments to invalid destinations, causing irreversible transaction failures and potential fund loss.

## Remediation

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.

```ts
// 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/`.

## Detection

- **ID:** `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`:**
  ```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`:**
  ```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'),
    }),
  ]);
  ```

## External references

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

Taxons: injection-and-input-trust

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