Skip to content

fix: check IBAN country code and length - #8

Merged
pavish merged 1 commit into
mainfrom
feat/iban-country-length
Nov 6, 2026
Merged

pavish merged 1 commit into
mainfrom
feat/iban-country-length

Conversation

@pavish

@pavish pavish commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

Summary

isValidIban accepts any compact candidate of 15 to 34 characters (2 letters, 2 digits, 11 to 30 letters or digits) with a valid mod-97 checksum. A random candidate of that shape passes the checksum 1 time in 97, so upper-case hex ids and codes that start with two letters and two digits are still taken for IBANs now and then.

A candidate must now also:

  • start with a country code that issues IBANs, and
  • have that country's length in compact form (spaces removed; e.g. DE 22, AT 20, CH 21, NO 15, LC 32).

The check runs inside isValidIban, before the checksum, so the existing prefix handling in replaceIban (DE89 3704 … 00 BIC …) works as before.

Country table (src/iban-countries.ts):

  • 97 countries of the SWIFT IBAN registry.
  • 27 countries that issue IBANs outside the registry (mostly West and Central Africa).
  • Source: ibantools 4.5.4 countrySpecs and iban 0.0.14 (the package behind @epilot/validators). The two agree on every registry length.
  • For three non-registry countries they disagree (BF 27/28, BI 16/27, DZ 24/26); both lengths are accepted.
  • Countries outside the registry are included deliberately: missing one would leave real IBANs unmasked, while an extra entry only allows a little more matching.

Behaviour change:

  • Not masked anymore: IBAN-shaped candidates with an unknown country code or the wrong length for their country, even when their checksum is valid.
  • Unchanged: valid IBANs of listed countries in any spacing the pattern accepts, and mistyped IBANs (rejected by the checksum already).
  • Not handled: IBANs of countries missing from the table. The table must be updated when a country starts issuing IBANs.

Effect on hex ids

scrub on 200,000 random ids per form. Counts are ids changed by the IBAN pattern; the phone pattern is unchanged by this PR and listed separately.

Id form IBAN matches, main IBAN matches, this PR Phone matches (both)
32 hex, upper case 98 (0.049%) 0 18–23 (about 0.01%)
EXIT-LOOP- + 32 hex, upper case 110 (0.055%) 0 15–21 (about 0.01%)
24 hex, upper case 116 (0.058%) 5 (0.003%) 11–18 (about 0.007%)
32 hex, lower case 0 0 16–23 (about 0.01%)
  • 32-character ids: no IBAN country has length 32, so these never pass now.
  • 24-character ids: the remaining matches start with AD (Andorra, length 24) and pass the checksum by chance.
  • Lower case: IBAN_PATTERN is upper case only, so lower-case ids never matched it.
  • Real IBANs: all 8 real IBANs in the same run (DE, AT, CH, GB, FR, NO; spaced, compact, followed by a BIC) are still masked.

Performance

main against this PR, each variant in its own process, 5 processes each; per process the median of 5 runs. Node 22, one machine.

Workload main median (range) This PR median (range) Difference
scrub, 100k texts with an IBAN and BIC 1134 ms (1082–1237) 1020 ms (954–1116) −10%
scrub, 100k invalid IBAN-shaped codes 255 ms (242–304) 151 ms (146–161) −41%
scrub, 100k upper-case 32-hex ids 123 ms (114–130) 120 ms (106–122) −2%
scrub, 100k labels without candidate 29 ms (27–34) 28 ms (27–32) −5%
anonymizeResponse, 16k mixed strings (as in performance.test.ts) 94 ms (91–103) 97 ms (85–99) +3%

Why it got faster: the country and length check costs one object lookup per candidate and runs before the mod-97 loop. Candidates that fail it skip the checksum, including the longer prefixes replaceIban tries when an IBAN is followed by text (… 00 BIC COBADEFFXXX). That's why text with an IBAN and invalid IBAN-shaped codes got faster.

The other rows are within the range of the runs. Memory was not measured.

Test plan

  • npm run lint, npm test (99 existing + 3 new tests), npm run build
  • New tests in value-detection.test.ts:
    • IBANs of CH, GB, FR (with letters), NO (15) and LC (32), plus a non-registry country (SN), are accepted;
    • with valid check digits, an unknown country code and DE with 21 or 23 characters are rejected;
    • a 32-character hex id with valid check digits is not masked.
  • CI (Node 18, 20, 22, 24)

🤖 Generated with Claude Code

https://claude.ai/code/session_01E3AAifuwHuKGdoVpqZoQAo

isValidIban accepted any 15 to 34 character candidate with a valid mod-97
checksum. A random candidate passes the checksum 1 time in 97, so hex ids
and other codes that start with two letters and two digits were still
taken for IBANs now and then.

A candidate must now also start with a country code that issues IBANs and
have that country's length (compact form, spaces removed). The table has
the 97 countries of the SWIFT IBAN registry and 27 countries that issue
IBANs outside it, taken from ibantools 4.5.4 and iban 0.0.14; where the
two disagree on a non-registry country, both lengths are accepted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E3AAifuwHuKGdoVpqZoQAo
@pavish pavish changed the title feat: check IBAN country code and length fix: check IBAN country code and length Nov 5, 2026
@pavish
pavish marked this pull request as ready for review November 5, 2026 11:30
@pavish
pavish merged commit 24c72dc into main Nov 6, 2026
5 checks passed
@pavish pavish mentioned this pull request Nov 9, 2026
1 task done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant