sota.io
Join the waitlist
2026-09-04·11 min read·sota.io team

CNIL's €500K Fine for a Missing MFA Toggle: What 5 Days of Undetected Access Really Means for Your Stack

CNIL's €500K Fine for a Missing MFA Toggle: What 5 Days of Undetected Access Really Means for Your Stack

On 3 September 2026, France's data protection authority CNIL published a €500,000 sanction against Hôpital Privé de la Loire, a private hospital, over a breach that gave an attacker unsupervised access to its patient file system for several days during summer 2025. The English-language summary on CNIL's sanctions list confirms the same figures. This is not a story about a hospital forgetting to buy a firewall. It's a story about two separate, ordinary engineering decisions — no MFA on an external login, no alerting on abnormal read volume — that together turned a stolen password into a five-day, half-million-record exfiltration nobody noticed until it was over.

If you build or operate any SaaS product that touches health data, employee records, or anything else GDPR classifies as a special category under Article 9, this decision is worth reading closely — not for the headline fine, but for exactly which two controls CNIL named as missing, and what a working version of each one actually looks like in code.

What CNIL actually found

The facts, as stated in CNIL's own decision:

Two things stand out if you read this as an engineer rather than a compliance officer. First, CNIL didn't fine the hospital for having a breach — breaches happen to well-run organizations too. It fined the hospital for a specific, named, avoidable gap: no MFA, no VPN, on a system holding special-category health data. Second, and more interesting for anyone building detection tooling: the sanction explicitly calls out the absence of anomaly detection as its own finding, separate from the authentication gap. A five-day, half-million-record read spike from a single account is not a subtle signal. It's the kind of thing a rate threshold on a log pipeline would have caught on day one.

Why this is a detection story, not just an authentication story

It's tempting to read "no MFA" as the whole lesson and stop there. That undersells what actually went wrong. MFA is a prevention control — it raises the cost of using a stolen password. But prevention controls fail. Credentials get phished, reused, or leaked from an unrelated breach, and no MFA implementation is bulletproof (SIM-swap and MFA-fatigue attacks are well documented). The reason a single compromised login turned into 727,113 exposed people's data over multiple days is that nothing downstream of the login noticed. Prevention and detection are two independent layers under GDPR Art.32(1)(b), which requires "the ability to ensure the ongoing confidentiality, integrity, availability and resilience of processing systems" — resilience here means catching a compromise in progress, not just keeping attackers out in the first place. CNIL's decision treats both as distinct obligations because they are: an org can have excellent MFA and still fail Art.32 if it has zero visibility into what an authenticated session actually does once it's inside.

That framing matters because MFA is now table stakes almost everywhere, but real-time access anomaly detection on the actual data layer — as opposed to network-perimeter monitoring — is still something most small and mid-size SaaS teams have never built. It's the control CNIL is signaling it will look for next.

Control 1: MFA is a starting point, not the finish line

If your product exposes any interface — admin panel, API with elevated scopes, or a patient/customer record view — to the public internet with only a password gate, you have the exact configuration CNIL cited. The fix is well understood, but worth being explicit about the shape of it: MFA needs to be enforced server-side, on every session-issuing path, not just the primary login UI (a common gap is a "legacy" API key or password-reset flow that bypasses the MFA check entirely).

A minimal enforcement pattern for a Node/Express service, using TOTP so it works with any standard authenticator app:

import { authenticator } from "otplib";

// Called after primary password verification succeeds.
// Session is only issued once BOTH factors pass.
async function verifySecondFactor(req, res, next) {
  const { userId, totpCode } = req.body;
  const user = await db.users.findById(userId);

  if (!user.mfaSecret) {
    // Fail closed: accounts without MFA enrolled cannot reach
    // systems handling special-category data (GDPR Art.9).
    return res.status(403).json({ error: "mfa_enrollment_required" });
  }

  const valid = authenticator.check(totpCode, user.mfaSecret);
  if (!valid) {
    await auditLog.record({ userId, event: "mfa_failed", ip: req.ip });
    return res.status(401).json({ error: "invalid_totp" });
  }

  req.session.mfaVerifiedAt = Date.now();
  next();
}

The important line is the "fail closed" branch: an account without MFA enrolled should not be able to reach a system with health or other special-category data at all, not fall back to password-only. That's the difference between "MFA is available" (what most breached orgs already had) and "MFA is required" (what CNIL's decision actually demands).

Control 2: catching a slow, quiet exfiltration in progress

This is the control almost nobody builds until after an incident like this one. The signal you're looking for isn't sophisticated — a single account reading an abnormal volume of records over a short window is a strong anomaly regardless of what your normal traffic pattern looks like. A simple rolling-window detector catches exactly the pattern CNIL described ("several days," "very large volume"):

from collections import defaultdict
from datetime import datetime, timedelta

# access_events: [{user_id, record_id, timestamp}, ...] from your app's
# audit log — every patient/customer record read, not just writes.
def flag_bulk_access_anomaly(access_events, window_hours=24, threshold=200):
    """
    Flags any account whose distinct-record read count in a rolling
    window exceeds `threshold` — tune per role, not globally: a billing
    clerk's baseline is very different from a treating physician's.
    """
    by_user = defaultdict(list)
    for e in access_events:
        by_user[e["user_id"]].append(e)

    alerts = []
    for user_id, events in by_user.items():
        events.sort(key=lambda e: e["timestamp"])
        window_start = 0
        seen_records = set()
        for i, e in enumerate(events):
            while events[i]["timestamp"] - events[window_start]["timestamp"] > timedelta(hours=window_hours):
                window_start += 1
            seen_records = {ev["record_id"] for ev in events[window_start:i + 1]}
            if len(seen_records) > threshold:
                alerts.append({
                    "user_id": user_id,
                    "distinct_records": len(seen_records),
                    "window_end": e["timestamp"],
                })
                break  # one alert per user is enough to trigger a review
    return alerts

Three implementation notes that separate a real control from a compliance-theater one:

Control 3: reduce what a compromised session can actually take

Detection and prevention both matter, but the third lever — data minimization at the access-control layer — determines how bad a successful breach is even when the first two controls both fail. If every authenticated clinician session can query the entire 524,867-record database rather than the specific patient panel they're treating, a single compromised account has maximum blast radius by design. Scoping reads to a need-to-know set (patients currently assigned to that provider, records touched in the last N days, whatever your domain's legitimate access pattern actually is) doesn't stop an attacker from using a stolen credential — but it caps what they can take with it, which is exactly the kind of "appropriate technical measure... appropriate to the risk" language in Art.32(2) that CNIL is evaluating against.

Why Article 34 is the part teams underestimate

CNIL cited Art.34 as a secondary violation alongside Art.32. Article 34(1) requires notifying affected individuals directly, without undue delay, "when the personal data breach is likely to result in a high risk to the rights and freedoms of natural persons." For most SaaS breaches, "high risk" is a judgment call your legal team argues about. For health data — a special category under Art.9 — that argument mostly doesn't exist: a breach exposing hundreds of thousands of patient records is close to a textbook case of high risk, which makes individual notification close to mandatory rather than discretionary. Teams that build products touching health, biometric, or similarly sensitive data should assume Art.34 notification is the default outcome of any meaningful breach, not an edge case to be argued down after the fact — and should have a notification runbook that doesn't start from scratch during an active incident.

What this means if you host or build EU healthcare SaaS

None of the three controls above are exotic, and none of them require a particular hosting provider — you can build MFA enforcement, read-anomaly detection, and scoped access control on any infrastructure. What changes with your hosting choice is a narrower, but still real, piece of the puzzle: where the audit logs, session data, and the anomaly-detection pipeline itself physically live and under whose legal jurisdiction. A US-headquartered cloud provider subject to the CLOUD Act can be compelled to hand over that data regardless of where the servers sit; an EU-domiciled provider with no US parent removes that exposure entirely. That's the specific, narrow claim sota.io's EU-sovereignty positioning makes — it's a piece of the Art.32/44 puzzle around where data and its access trail live, not a substitute for the application-layer controls above. Hosting location alone would not have stopped this breach; the missing MFA and missing anomaly detection would have failed identically on any cloud, EU or not. Both layers — infrastructure jurisdiction and application-layer security — are independently necessary, and CNIL's decision is a reminder that regulators are now willing to price the application-layer gap at half a million euros on its own.

A checklist before your next audit

CNIL's decision gives every team building on top of sensitive EU data a specific, dated, primary-sourced answer to "what does 'appropriate technical measures' actually mean in practice" — and it's a shorter list than most compliance checklists suggest: MFA that's actually required, and someone (or something) watching what happens after login.

EU-Native Hosting

Ready to move to EU-sovereign infrastructure?

sota.io is a German-hosted PaaS — no CLOUD Act exposure, no US jurisdiction, full GDPR compliance by design. Deploy your first app in minutes.