Skip to content

isTaxID(dk-DK): valid CPR numbers wrongly rejected for century-digit 5-8, year 37-58 #2818

Description

@alanturing881

Description

isTaxID(str, 'dk-DK') (Danish CPR-nummer) incorrectly rejects syntactically- and checksum-valid CPR numbers whenever the 7th digit (century digit) is 5, 6, 7, or 8 and the 2-digit birth year is in the range 37-58 (inclusive).

Root cause

In src/lib/isTaxID.js, dkDkCheck:

switch (century_digit) {
  case '0':
  case '1':
  case '2':
  case '3':
    year = `19${year}`;
    break;
  case '4':
  case '9':
    if (year < 37) {
      year = `20${year}`;
    } else {
      year = `19${year}`;
    }
    break;
  default: // century_digit is 5, 6, 7, or 8
    if (year < 37) {
      year = `20${year}`;
    } else if (year > 58) {
      year = `18${year}`;
    } else {
      return false;
    }
    break;
}

The case '4': case '9': branch correctly implements the documented rule for those digits (year 00-36 → 2000s, year 37-99 → 1900s).

For century digits 5, 6, 7, 8, the documented rule (see e.g. the century-digit table reproduced at https://blog.ploeh.dk/2018/12/10/danish-cpr-numbers-in-f/, sourced from the Danish Wikipedia CPR-nummer article) is a clean split with no gap: year 00-57 → 2000s, year 58-99 → 1800s.

The default branch instead reuses the wrong thresholds from the 4/9 case (year < 37 / year > 58), leaving years 37-58 falling into an else { return false; } that shouldn't exist at all — every 2-digit year should map to exactly one century for these digit values.

Reproduction (validator 13.15.35, latest on npm at time of writing)

Using checksum-valid constructed CPR numbers (day=01, month=01, century digit 5, correct check digit computed with the library's own weight formula):

const v = require('validator');
console.log(v.isTaxID('0101365018', 'dk-DK')); // true  (year 36, correct)
console.log(v.isTaxID('0101375005', 'dk-DK')); // false <-- should be true (Jan 1 2037)
console.log(v.isTaxID('0101455009', 'dk-DK')); // false <-- should be true (Jan 1 2045)
console.log(v.isTaxID('0101575004', 'dk-DK')); // false <-- should be true (Jan 1 2057)
console.log(v.isTaxID('0101585018', 'dk-DK')); // false <-- should be true (Jan 1 1858)
console.log(v.isTaxID('0101595005', 'dk-DK')); // true  (year 59, correct: Jan 1 1859)

Impact

Any application using isTaxID(input, 'dk-DK') will wrongly reject legitimate CPR numbers for:

  • People born in 1858 (year=58 exactly — a case the CPR system explicitly supports; its historical documentation cites a real 1863 registrant, so 1858 is within the system's intended range) — affects historical-records validation today.
  • Anyone issued a CPR number with a birth year of 2037-2057 under century digits 5-8 — will start affecting real newly-issued numbers as those years arrive.

This is a false-negative / over-rejection bug, not a validation bypass — no security impact, just incorrect application behavior for the affected date ranges.

Suggested fix

default: // century_digit is 5, 6, 7, or 8
  if (year <= 57) {
    year = `20${year}`;
  } else {
    year = `18${year}`;
  }
  break;

This matches the documented gap-free century-digit table for 5-8 and removes the erroneous rejection branch.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions