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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions