medicus.global · 2026-05-05
"Alle informationer har jeg selv indført, og jeg er indforstået med at
redningsmandskab og læger kan læse dem — for at give dem den bedst
mulige forhåndsviden om min helt personlige situation, særligt i en
nødsituation. Min kærlighed til livet er større end landets regler
om datasikkerhed."
Det er den voksne, frivillige beslutning der ligger bag enhver oprettet My Medicus-profil. Vores opgave er at holde patientens valg i hævd: opbevare data forsvarligt, vise det kun til den der har korrekt PIN, lade patienten se hver scan, og lade patienten revoke kortet i sekundet det er mistet. Resten af dette dokument forklarer hvordan.
"Hvad er jeres sikkerhed i forbindelse med My Medicus?"
I en nødsituation er patientens største ønske at blive rask igen. Det er den vi tjener. Sikkerhed er midlet, ikke målet.
Tre principper står over alt andet:
Vi har dokumenteret 19 forskellige angrebs-scenarier mod My Medicus og kørt hver enkelt mod produktions-systemet. Resultatet er beskrevet nedenfor — uden marketing-sprog, uden "industri-standard", uden "best practice". Bare hvad der konkret er testet.
19 angrebs-scenarier mod to konti i den live database — én demo-konto og én ægte ny bruger. Værktøjer: direkte HTTP-requests, parallel-eksekvering, SQL-introspection af produktions-databasen, parsing af responses.
Resultat: 14 angreb mødte forsvar der holdt. 5 sårbarheder blev fundet og lukket samme dag.
| Angreb | Hvorfor angrebet fejlede |
|---|---|
| Tyveri af hele databasen | Allergier, medicin, CPR, NIE, passport, EHIC og forsikringsnumre er krypteret med AES-256-GCM. Nøglen ligger i en separat secret-store, aldrig i databasen. Stjælder du DB-filen, får du ulæselig tekst |
| Brute-force på 6-cifret PIN | Progressive forsinkelser kicker ind ved 5. forkerte forsøg: 5 sek → 30 sek → 60 sek mellem hver |
| SQL-injection på login-formular | Alle databaseforespørgsler bruger parameterized queries — fem forskellige injection-payloads blev afvist |
| Direkte API-kald uden login | Alle beskyttede endpoints returnerer HTTP 401 |
| Token-gæt | Tokens er 32 hex-tegn afledt via SHA-256. Søgerum: 16^32. 50 tilfældige gæt → 0 treffere |
| Path-traversal til backups eller config | /_db_backups/, /.env, /.git, /medicus.db, /wp-config.php returnerer alle 404 |
| Email-enumeration via "glemt password" | Konsistent generisk svar uanset om emailen findes — ingen info-leak |
| XSS i offentlige felter | Server afviser malformed input, ingen reflektion af HTML/JS |
| Fysisk wallet-tyveri uden kortets PIN | PIN-gate stopper alle ikke-korrekte forsøg |
| 10 parallelle brute-force forsøg | Rate-limit gælder per token, ikke per request |
| Sletning af audit-log via DELETE-endpoint | Ingen DELETE-endpoints eksponeret |
Gæt af admin-token (admin, 1234, letmein) |
Alle returnerer 401 |
| Forsøg på at få server til at lække secrets via fejl-payloads | Hverken krypteringsnøgle eller Stripe-secrets dukker op i error-output |
| Cross-origin abuse fra anden hjemmeside | Browser blokerer pga. same-origin policy |
| # | Hvad | Hvor alvorligt | Hvad blev gjort |
|---|---|---|---|
| F1 | Login svarede 200 ms langsommere når emailen var registreret end når den ikke var. En tyv kunne dermed afgøre om en konto findes | 🔴 Kritisk | Login kører nu Argon2id-verifikation altid (også mod ukendte emails, med en dummy-hash). Konstant svar-tid. Generisk fejl-tekst |
| F2 | Ingen security-headers (HSTS, CSP, X-Frame, X-Content-Type, Referrer-Policy, Permissions-Policy) | 🔴 Kritisk | Alle 6 headers tilføjes nu på samtlige responses via middleware. CSP låser script/img/style/frame til hvidlistede sources |
| F3 | Et stjålet kort har QR + PIN trykt direkte på det. Tyven får adgang indtil ejer revoker | 🟠 Høj | (1) Patient ser hver scan i sit dashboard ("Min log" — IP-prefix, land, tidspunkt). (2) Ejer kan på ét klik revoke kortet → alle gamle printede kort dør øjeblikkeligt + ny PIN genereres |
| F4 | /admin returnerede SPA-HTML til alle, hvilket afslørede admin-panelets eksistens |
🟡 Lav | Returnerer nu samme 404 som ikke-eksisterende routes for ikke-autentificerede brugere |
| F5 | Forgot-password kunne misbruges til at oversvømme et offers indbakke (1.000 reset-mails/time) — og samtidig skade vores egen sender-reputation | 🟠 Høj | Hvis brugeren har en gyldig reset-token (max 2 timer gammel), genbruges den i stedet for at sende ny mail. Plus per-email rate-limit på 2/min |
| Valg | Begrundelse |
|---|---|
| Ingen MitID på paramedic-view | Patienten ligger bevidstløs på golfbanen. Paramedicineren har ikke MitID, og selv hvis de havde, har vi ikke 90 sekunder til at logge dem ind. PIN-på-kort er det praktiske svar |
| Ikke "zero-knowledge" | Serveren KAN dekryptere felterne — det er en betingelse for at paramedic kan se data. Vi kalder ikke noget "zero-knowledge" som ikke er det |
| Ingen automatisk data-import | Hvert felt er valgt af brugeren. Ingen auto-import fra andre sundhedsplatforme |
Hvis en paramedic legitimt scanner kortet med korrekt PIN, kan vedkommende fotografere skærmen. Vi kan ikke forhindre det — det er prisen for at data overhovedet kan vises i en nødsituation. Defense:
Det er samme sikkerheds-model som et fysisk medical-ID-armbånd hos diabetikere: data er synlig for redningsmandskab, vi accepterer at en lommetyv også kan se den. Forskellen er at vores model giver patienten kontrol over hvem der ser hvad og hvornår — ikke kun synlighed.
Spørgsmål? Skriv til peter@medicus.global