medicus.global · 2026-05-05
"Toda la información la he introducido yo mismo, y consiento que
el personal de emergencias y los médicos puedan leerla — para darles
el mejor conocimiento previo posible de mi situación personal,
especialmente en una emergencia. Mi amor por la vida es mayor que
las normas del país sobre protección de datos."
Esa es la decisión adulta y voluntaria detrás de cada perfil My Medicus creado. Nuestra tarea es honrar la elección del paciente: guardar los datos de forma segura, mostrarlos solo a quien tiene el PIN correcto, dejar que el paciente vea cada escaneo, y permitir que el paciente revoque la tarjeta en el segundo en que se pierde. El resto de este documento explica cómo.
"¿Cuál es vuestra seguridad respecto a My Medicus?"
En una emergencia, el mayor deseo del paciente es recuperarse. Eso es a quien servimos. La seguridad es el medio, no el fin.
Tres principios están por encima de todo:
Hemos documentado 19 escenarios de ataque contra My Medicus y hemos ejecutado cada uno contra el sistema en producción. El resultado se describe abajo — sin lenguaje de marketing, sin "estándar de la industria", sin "mejores prácticas". Solo lo que se ha probado concretamente.
19 escenarios de ataque contra dos cuentas en la base de datos en vivo — una cuenta demo y un usuario real nuevo. Herramientas: peticiones HTTP directas, ejecución paralela, introspección SQL de la base de datos de producción, parsing de respuestas.
Resultado: 14 ataques se encontraron con defensas que aguantaron. 5 vulnerabilidades fueron encontradas y cerradas el mismo día.
| Ataque | Por qué falló el ataque |
|---|---|
| Robo de toda la base de datos | Alergias, medicación, números de identidad, pasaporte, EHIC y números de seguro están cifrados con AES-256-GCM. La clave vive en un almacén de secretos separado, nunca en la base de datos. Si robas el archivo DB, obtienes texto ilegible |
| Fuerza bruta sobre PIN de 6 dígitos | Retrasos progresivos se activan al 5º intento incorrecto: 5 seg → 30 seg → 60 seg entre cada uno |
| Inyección SQL en formulario de login | Todas las consultas a la base de datos usan consultas parametrizadas — cinco payloads de inyección distintos fueron rechazados |
| Llamadas API directas sin login | Todos los endpoints protegidos devuelven HTTP 401 |
| Adivinanza de tokens | Los tokens son 32 caracteres hex derivados vía SHA-256. Espacio de búsqueda: 16^32. 50 intentos aleatorios → 0 aciertos |
| Path-traversal a backups o config | /_db_backups/, /.env, /.git, /medicus.db, /wp-config.php devuelven todos 404 |
| Enumeración de emails vía "olvidé contraseña" | Respuesta genérica consistente independientemente de si el email existe — sin fuga de información |
| XSS en campos públicos | El servidor rechaza entrada malformada, sin reflexión de HTML/JS |
| Robo físico de cartera sin el PIN de la tarjeta | El bloqueo PIN detiene todos los intentos no correctos |
| 10 intentos paralelos de fuerza bruta | El rate-limit aplica por token, no por petición |
| Borrado del registro de auditoría vía endpoint DELETE | No hay endpoints DELETE expuestos |
Adivinanza de admin-token (admin, 1234, letmein) |
Todos devuelven 401 |
| Intentar que el servidor filtre secretos vía payloads de error | Ni clave de cifrado ni secretos de Stripe aparecen en la salida de error |
| Abuso cross-origin desde otro sitio | El navegador bloquea por política same-origin |
| # | Qué | Severidad | Qué se hizo |
|---|---|---|---|
| F1 | Login respondía 200 ms más lento cuando el email estaba registrado que cuando no. Un atacante podía determinar si una cuenta existe | 🔴 Crítico | Login ahora ejecuta verificación Argon2id siempre (también contra emails desconocidos, con un hash dummy). Tiempo de respuesta constante. Mensaje de error genérico |
| F2 | Sin cabeceras de seguridad (HSTS, CSP, X-Frame, X-Content-Type, Referrer-Policy, Permissions-Policy) | 🔴 Crítico | Las 6 cabeceras se añaden ahora en todas las respuestas vía middleware. CSP bloquea script/img/style/frame a sources en lista blanca |
| F3 | Una tarjeta robada tiene QR + PIN impresos directamente en ella. El ladrón obtiene acceso hasta que el dueño revoque | 🟠 Alto | (1) Paciente ve cada escaneo en su panel ("Mi registro" — prefijo IP, país, hora). (2) Dueño puede con un clic revocar la tarjeta → todas las tarjetas impresas mueren al instante + se genera nuevo PIN |
| F4 | /admin devolvía SPA-HTML a todos, lo que revelaba la existencia del panel admin |
🟡 Bajo | Devuelve ahora el mismo 404 que rutas inexistentes para usuarios no autenticados |
| F5 | Forgot-password podía abusarse para inundar la bandeja de un objetivo (1.000 mails/hora) — y a la vez dañar nuestra reputación de remitente | 🟠 Alto | Si el usuario tiene un token reset válido (máx 2 horas), se reutiliza en lugar de enviar nuevo mail. Plus rate-limit por email de 2/min |
| Elección | Razón |
|---|---|
| Sin autenticación nacional en la vista paramédica | El paciente está inconsciente en el campo de golf. El paramédico no tiene autenticación nacional, y aunque la tuviera, no tenemos 90 segundos para entrarles. PIN-en-tarjeta es la respuesta práctica |
| No "zero-knowledge" | El servidor PUEDE descifrar los campos — es condición para que el paramédico pueda ver datos. No llamamos "zero-knowledge" a algo que no lo es |
| Sin importación automática de datos | Cada campo es elegido por el usuario. Sin auto-importación desde otras plataformas de salud |
Si un paramédico escanea legítimamente la tarjeta con el PIN correcto, puede fotografiar la pantalla. No podemos impedirlo — es el precio de que los datos puedan mostrarse en una emergencia. Defensa:
Es el mismo modelo de seguridad que una pulsera de identificación médica física para diabéticos: los datos son visibles para el personal de emergencias, aceptamos que un carterista también pueda verlos. La diferencia es que nuestro modelo da al paciente control sobre quién ve qué y cuándo — no solo visibilidad.
¿Preguntas? Escribe a peter@medicus.global