Start your free trial — no credit card required.
Denmark's ID Register Breached: 8.8 Million Exposed via a Two-Person Vendor's Login, Caught by an Invoice
Arjun12 min read

Denmark's ID Register Breached: 8.8 Million Exposed via a Two-Person Vendor's Login, Caught by an Invoice

14 million lookups ran through one small vendor's legitimate CPR access. Nobody noticed until the bill arrived. What a usage alarm should look like.

Share:

Somewhere in Odense, a company with two employees on its business registration opened a bill for CPR register lookups and found a number that made no sense. That bill is how Denmark found out that the names, addresses and personal ID numbers of about 8.8 million people had been pulled out of its national population register.

Not an IDS. Not a SIEM rule. An invoice.

The Ministry of Research, Higher Education and Digitalisation announced the breach on October 5. According to the ministry's press release and the timeline compiled from DR and TV 2 reporting, roughly 14 million lookups ran against the CPR register between September 11 and September 20, all through the lawful access held by Pays ApS, a small Funen IT firm that sells data cleaning and enrichment software. Pays' owner, Sophie Laursen, confirmed to TV 2 Fyn that the company's legitimate access had been abused and that Pays did not perform the searches itself.

I've spent a lot of time writing detections for "a valid account doing something it shouldn't." This case is the cleanest example I've seen of why that's hard, and of where the signal actually was.

Run the arithmetic on 14 million

Fourteen million queries in about ten days works out to roughly 1.4 million a day. That's about 58,000 an hour, or 16 every second, sustained, from one customer account.

Denmark has around six million living residents. The 8.8 million figure is larger because, per the ministry, it includes deceased people and people who have emigrated. So whoever was behind this wasn't looking up a list of targets. They were walking the number space.

That matches how the Danish Data Protection Agency characterised the activity, as reported by eBuilder Security: a very large number of automated searches aimed at finding valid CPR numbers. A CPR number is a birth date plus four digits. The space is structured and guessable, and any interface that answers "does this number exist, and who is it?" turns into an enumeration oracle once someone points a script at it.

Now picture what Pays' normal usage looked like. A two-person data-enrichment shop serving nonprofits and public bodies might plausibly run thousands of lookups in a busy month. Maybe tens of thousands. The jump to 14 million is three or four orders of magnitude. You don't need machine learning to spot that. You need anyone, or anything, to be looking.

The password story, and why it's a distraction

On October 9, an anonymous person told Politiken they were behind the attack and that a leaked password from a former employee was enough to get in. Politiken then reported that at least three Pays profiles used the password "123456," including the company's administrator account. Ritzau, in a report carried by Se og Hør, repeated the same account. Neither the password claim nor the attacker's identity has been officially confirmed, and the National Unit for Special Crime (NSK) has named no suspect.

Everyone will fixate on "123456." It's a great headline. It's also the least interesting part.

Assume Pays had used a 24-character random password. Assume it had MFA. The attacker's claim is that they had a former employee's credentials. Offboarding failures, infostealer logs and reused passwords all produce the same outcome: someone outside the company holding a login that works. We've written before about the service accounts nobody owned in the Chilean password spray, and the pattern is identical. Credentials leak. They always leak. The question that decides how bad the breach gets is what happens in the minutes and hours after a valid login starts behaving strangely.

Here, the answer was: nothing, for about two weeks.

A disputed timeline that's bad either way

The reporting doesn't agree on how long the door stayed open, and the disagreement itself is telling.

  • TV 2 reported that the attacker had access for 21 days and 17 hours, from September 10 until the access was cut on October 2.
  • The CPR office's notification to Datatilsynet, as summarised by Danish media, puts the end of the unauthorised activity at September 20.
  • Detection came on October 2, when, per DR, the invoice exposed the abnormal search volume and the CPR office shut off Pays' access.

Take the generous reading. The lookups stopped on September 20 and the register operator found out twelve days later. That means the entire extraction happened, finished, and sat undiscovered for well over a week. Jacob Herbst, chair of Denmark's Cybersecurity Council, told DR it was "ret chokerende," pretty shocking, that billing was the detection mechanism.

He's right. But I'd push it further. Billing was the only system in the whole chain that measured per-customer volume at all. Finance noticed because finance was the only team counting.

1,039 doors, one lock design

The government's response tells you how the register was built. In the days after disclosure, the CPR administration sent a letter to 1,039 companies with CPR access, reminding them of security rules, according to a Ritzau report carried by Se og Hør. Minister Christina Egelund said it was "quite obvious that security in general has not been good enough" and ordered a review of the whole system.

The letter's requirements are worth reading closely:

  1. Appoint a security officer.
  2. Regularly review employees' access to register data.
  3. Track the number of monthly CPR transactions to detect misuse.

Item three is the whole breach in one sentence. The control that would have caught this was pushed to the 1,039 customers, and it was framed as monthly. A monthly counter would have caught this attack at roughly the same moment the invoice did.

This is the same design flaw we picked apart in Florida's DAVID breach, where one police login reached 200,000 driver records. A central government data store hands out direct query rights to a long tail of third parties: banks, telecoms, law firms, unions, insurers, and small shops like Pays. Each one is trusted with the full query capability. The operator relies on contracts and audits to keep each tenant honest, and has little real-time view of how any single tenant behaves.

When the tenant is a large bank with a security team, that might be fine. When it's a two-person company whose administrator account allegedly used "123456," the register's security is exactly as good as that company's worst day.

Where I'd have put the tripwires

I want to be concrete, because "monitor for anomalies" is advice nobody can act on. Here are the signals in this case, ranked by how early they would have fired.

Tripwire 1: records nobody should ever query

This is the one I'd build first, and it's a deception control rather than an analytics one.

Seed the register with a small set of synthetic CPR records: valid-format numbers, plausible names and addresses, attached to people who don't exist. No legitimate customer has any reason to look them up, because no real customer, member or policyholder has those numbers. Spread them across birth dates and serial ranges so any sweep of the number space trips over them.

An enumerator walking millions of numbers hits a canary record within the first few thousand queries. The alert is binary. A lookup on a record that doesn't exist in the real world has no innocent explanation, so there's no tuning, no threshold debate and no alert fatigue. You get the account name and the timestamp, and you can cut access the same hour.

Notice what this doesn't depend on. It doesn't care whether the password was "123456" or a hardware key. It doesn't need a baseline for Pays' normal behaviour. It doesn't require the 1,039 customers to do anything. The register operator owns it entirely. It's the same principle behind the honeytokens and decoy credentials Mine2 builds for identity-based attacks: put something in the path that only an intruder would touch.

There's a real edge case here, and I'd rather name it than pretend it away. Some customers run batch validation jobs against whole customer files, and a dirty file could contain a mistyped CPR number that happens to match a canary. You handle that by choosing canary numbers from birth-date ranges that are very unlikely in a given customer's population, and by treating the first hit as "freeze and call" rather than "revoke and report to police." Even with that caution, you're on the phone with the vendor on day one instead of day twenty-two.

Tripwire 2: rate per tenant, measured in hours

The CPR office already knew each customer's volume, because it billed on it. Moving that counter from a monthly invoice run to an hourly check is a plumbing change, not a research project.

The rule is dull: alert when a tenant's hourly lookups exceed some multiple of its trailing 90-day hourly maximum. For Pays, a ceiling of even 50 times its historical peak would still have fired within the first hour of a 58,000-an-hour run.

Rate limits belong here too. Few legitimate integrations need 16 identity lookups per second, sustained, around the clock. Throttle above a contracted ceiling, and make tenants ask for more.

Tripwire 3: hit-rate on misses

Enumeration has a fingerprint that normal business use doesn't. When you guess numbers, many of your queries come back empty or invalid. When you look up customers you actually have, almost all of them resolve.

If the register tracked the ratio of not-found responses per tenant, a sweep would stand out even at a modest rate. A data-cleaning firm verifying real addresses should see a tiny miss rate. A script brute-forcing birth-date-plus-serial combinations will see a large one. This catches the slow, patient version of the attack that a volume threshold might miss.

Tripwire 4: the vendor's own house

Finally, Pays itself. If the attacker's account is accurate and a former employee's credentials worked, the gap was in offboarding and in the admin account. A decoy admin credential planted in the places attackers look (old config files, a password manager export, a scripts folder) gives a small vendor an alarm that costs nothing to run. Two-person companies don't have SOCs. They can still have a credential that rings someone's phone when it's used. Hardening the endpoints those credentials live on, the sort of baseline work covered by Fortify, shrinks the odds of the leak in the first place.

Of the four, I'd rank the canary records highest, because they're the only control that works no matter how the attacker got in and no matter which of the 1,039 tenants they came through.

The fallout lands on everyone else

The data itself is the boring part to an attacker: names, addresses, CPR numbers. Danes share CPR numbers constantly, and plenty of people already assume theirs is semi-public.

That's exactly the problem. The Danish Agency for Societal Security (Styrelsen for Samfundssikkerhed) put out guidance on October 6 telling authorities, organisations and companies not to release sensitive information based on CPR data alone and to add identity checks. In practice, a lot of Danish customer service and account recovery has leaned on "tell me your CPR number" as a proof of identity. As of this month, roughly the entire population's CPR number, name and address sit together in an unknown party's hands.

We made the same argument about 153 million license scans and why a photo ID can't be treated as proof of anything. An identifier is not an authenticator. Once it's leaked at population scale, every process that treated it as a secret has to be redesigned, and the phishing and vishing that follow will use those exact details to sound legitimate. Expect fake calls from "your bank" that open by reciting your CPR number and address correctly.

If you run a help desk or account recovery flow anywhere in the Nordics, this is the week to audit which steps still accept knowledge of a CPR number as a verification factor.

What I'd take from this if I ran any shared data service

You don't need to operate a national register for this to apply. Any platform that gives API or query access to many customers has the same shape: a payments processor, a credit bureau, a SaaS data enrichment API, a healthcare clearinghouse.

Ask three questions this week:

  • Do you measure per-tenant usage faster than you bill for it? If the only per-customer counter lives in finance, you have the Pays problem.
  • Do you have records in your dataset that no legitimate tenant would ever request? If not, you're relying on thresholds and baselines alone, and those need tuning, history and someone watching.
  • What does a tenant's account look like when its smallest, least secure user gets phished? The answer is the real security level of your platform.

The Danish case will probably end with a police report, a security review and stricter contracts for 1,039 companies. Those help. But the cheapest fix was available the whole time: a handful of fake people in the register, and an alarm on anyone who asked about them.

If you want to see how decoy records and honeytoken credentials would sit in front of your own shared data paths, book a Mine2 demo and we'll map where the first tripwire should go.

M2

Arjun

Lead Detection Engineer, Mine2

Arjun builds detection logic at Mine2, focusing on the blind spots EDR and SIEM leave behind and how honeytokens close them.

Share this article

Secure Your Network Today

Ready to implement advanced cyber deception in your organization? See how MINE2 can transform your threat detection capabilities.