medicus.global ยท 2026-05-05
"All the information was entered by me, and I understand that rescuers
and doctors can read it โ to give them the best possible advance
knowledge of my entirely personal situation, especially in an emergency.
My love for life is greater than the country's rules about data security."
That is the adult, voluntary decision behind every created My Medicus profile. Our job is to honour the patient's choice: keep the data secure, show it only to whoever holds the correct PIN, let the patient see every scan, and let the patient revoke the card the second it's lost. The rest of this document explains how.
"What security do you have for My Medicus?"
In an emergency, the patient's greatest wish is to recover. That's what we serve. Security is the means, not the end.
Three principles stand above everything else:
19 attack scenarios against two accounts in the live database โ one demo account and one real new user. Tools: direct HTTP requests, parallel execution, SQL introspection of the production database, response parsing.
Result: 14 attacks met defenses that held. 5 vulnerabilities were found and closed the same day.
| Attack | Why the attack failed |
|---|---|
| Theft of the entire database | Allergies, medication, national IDs, passport, EHIC and insurance numbers are encrypted with AES-256-GCM. The key lives in a separate secret store, never in the database. If you steal the DB file, you get unreadable text |
| Brute-force on 6-digit PIN | Progressive delays kick in at the 5th wrong attempt: 5 sec โ 30 sec โ 60 sec between each |
| SQL injection on login form | All database queries use parameterized queries โ five different injection payloads were rejected |
| Direct API calls without login | All protected endpoints return HTTP 401 |
| Token guessing | Tokens are 32 hex characters derived via SHA-256. Search space: 16^32. 50 random guesses โ 0 hits |
| Path traversal to backups or config | /_db_backups/, /.env, /.git, /medicus.db, /wp-config.php all return 404 |
| Email enumeration via "forgot password" | Consistent generic response regardless of whether the email exists โ no info leak |
| XSS in public fields | Server rejects malformed input, no reflection of HTML/JS |
| Physical wallet theft without the card's PIN | PIN gate stops all incorrect attempts |
| 10 parallel brute-force attempts | Rate-limit applies per token, not per request |
| Audit-log deletion via DELETE endpoint | No DELETE endpoints exposed |
Admin token guess (admin, 1234, letmein) |
All return 401 |
| Trying to make the server leak secrets via error payloads | Neither the encryption key nor Stripe secrets appear in error output |
| Cross-origin abuse from another website | Browser blocks via same-origin policy |
| # | What | Severity | What was done |
|---|---|---|---|
| F1 | Login responded 200 ms slower when the email was registered than when it wasn't. An attacker could thus determine if an account exists | ๐ด Critical | Login now runs Argon2id verification always (also against unknown emails, with a dummy hash). Constant response time. Generic error message |
| F2 | No security headers (HSTS, CSP, X-Frame, X-Content-Type, Referrer-Policy, Permissions-Policy) | ๐ด Critical | All 6 headers now added to all responses via middleware. CSP locks script/img/style/frame to whitelisted sources |
| F3 | A stolen card has the QR code + PIN printed directly on it. The thief gets access until the owner revokes | ๐ High | (1) Patient sees every scan in their dashboard ("My log" โ IP prefix, country, timestamp). (2) Owner can revoke the card with one click โ all old printed cards die instantly + new PIN generated |
| F4 | /admin returned SPA HTML to everyone, revealing the admin panel's existence |
๐ก Low | Now returns the same 404 as nonexistent routes for unauthenticated users |
| F5 | Forgot-password could be abused to flood a victim's inbox (1,000 reset emails/hour) โ and simultaneously damage our own sender reputation | ๐ High | If the user has a valid reset token (max 2 hours old), it is reused instead of sending a new email. Plus per-email rate-limit of 2/min |
| Choice | Reasoning |
|---|---|
| No national-ID login (MitID, BankID, etc.) on paramedic view | The patient is unconscious on the golf course. The paramedic doesn't have national-ID credentials โ and even if they did, we don't have 90 seconds to log them in. PIN-on-card is the practical answer that works globally |
| Not "zero-knowledge" | The server CAN decrypt the fields โ that's a precondition for paramedic access. We don't call anything "zero-knowledge" that isn't |
| No automatic data import | Every field is chosen by the user. No auto-import from other health platforms |
If a paramedic legitimately scans the card with the correct PIN, that person can photograph the screen. We cannot prevent it โ that's the price of having the data visible at all in an emergency. Defenses:
This is the same security model as a physical medical-ID bracelet for diabetics: data is visible to rescue personnel, and we accept that a pickpocket could also see it. The difference is that our model gives the patient control over who sees what and when โ not just visibility.
Questions? Write to peter@medicus.global