Seguridad Sanitaria MediTrack EHR v2.4 — CDSS Engine

Healthcare Review: Motor CDSS & API Clínica

Revisión de seguridad clínica, cumplimiento PHI e integridad de datos · 5 módulos analizados
Revisor CULTIVA IA — Skill Revisor Sanitario
Fecha 18 jun 2026
Stack Node.js · React · Supabase
Módulos drug-interactions · insulin · NEWS2 · encounter · schema
Pacientes en riesgo ~85.000 registros activos
🚨

BLOCK — PATIENT SAFETY RISK

El sistema NO debe desplegarse en producción. Se han detectado 3 fallos críticos de seguridad del paciente: fallo silencioso en el motor de interacciones farmacológicas, alerta de sobredosis de insulina bypasseable, y exposición masiva de PHI en logs y URLs. Requiere corrección inmediata antes de cualquier uso clínico.

Patient Safety Impact
CRITICAL
3
Críticos — Seguridad Paciente
4
Altos — PHI / Clínicos
2
Medios — Técnicos
8
Checks superados
Checklist de Verificación

🧠 Motor CDSS

Interacciones sin respuesta ante fallo del lookup (retorna vacío en catch — false negative)
Alerta de sobredosis de insulina es toast dismissable — no bloquea prescripción
!
NEWS2: SpO2 rango 92–93 retorna score 2, spec RCP marca 1 para 92–93% en escala 1
NEWS2: rango temperatura correcto según spec RCP
NEWS2: AVPU penalización +3 si no A — correcto
Tipo de insulina desconocido asume max=100U por defecto — no debe permitirse sin validación

🔒 Protección PHI

patient_id en console.log/console.error dentro de checkInteractions (exposición en logs de servidor)
Resultado completo de búsqueda de pacientes logueado con JSON.stringify (nombre, DNI, historial)
Parámetro ?dob= (fecha de nacimiento) en query string de URL — PHI en logs de acceso HTTP
Sin RLS en tablas patients, medications, encounters — cualquier sesión autenticada accede a todos los datos
Sin service_role en código cliente detectado
Sin PHI en localStorage/sessionStorage detectado en fragmentos revisados

📋 Flujo Clínico

Encuentros editables hasta 24h sin generar addendum — modificación directa de registros clínicos
Sin entrada en audit trail para UPDATE de encounters
Alerta CDSS retorna array (procesable programáticamente)
!
Sin override reason logging cuando clínico bypasea alerta de insulina

🗄️ Integridad de Datos

ON DELETE CASCADE en medications y encounters — borrar paciente elimina historia clínica
!
Sin detección de edición concurrente en endpoint PUT /encounters
UUIDs para todas las PKs — correcto
Timestamps con TIMESTAMPTZ (timezone-aware) — correcto
Consultas parametrizadas en encounters API ($1, $2) — sin SQL injection
SQL injection en checkInteractions: interpolación directa de patientId sin parametrizar
Mapa de Exposición PHI
Vector Archivo Datos expuestos Estado Riesgo LOPDGDD / ENS
console.log línea 8 drug-interactions.js patient_id + combinación de fármacos EXPUESTO Infracción muy grave Art. 83.5 RGPD — hasta €20M
console.error línea 12 drug-interactions.js patient_id en stack trace de errores EXPUESTO Infracción muy grave — datos de salud (cat. especial)
Query string URL encounter-api.js ?name=... &dob=... en GET /api/patients/search EXPUESTO PHI en logs de acceso de proxy/LB/CDN
console.log línea 18 encounter-api.js JSON.stringify de registros completos de pacientes (nombre, NIF, historial) EXPUESTO Brecha de datos Art. 33 RGPD — notificación AEPD <72h
RLS deshabilitado schema.sql Tablas patients, medications, encounters sin políticas Row-Level Security SIN AISLAR Cross-facility data leak — violación segregación datos
Audit trail ausente encounter-api.js Modificaciones de historial sin registro de quién/cuándo/qué cambió INCOMPLETO Incumplimiento Art. 32 RGPD — medidas técnicas adecuadas
Detalle de Incidencias
CRÍTICO PATIENT SAFETY Fallo silencioso en detector de interacciones farmacológicas
Impacto
Si el lookup de interacciones falla (red, timeout, error de BD), la función retorna [] como si no hubiera interacciones. Un médico prescribiendo warfarina a un paciente con AINEs no recibiría alerta. Potencial hemorragia grave o muerte. Un único false negative es peor que mil false positives.
Corrección requerida
Propagar el error o retornar un estado explícito de error. Nunca silenciar excepciones en lógica CDSS.
// CORRECTO: propagar error, nunca silenciar } catch(e) { logger.error('CDSS_FAIL', { module: 'drug-interactions' }); throw new CDSSUnavailableError('Interaction check failed'); }
CRÍTICO PATIENT SAFETY Alerta de sobredosis de insulina no bloquea prescripción (toast dismissable)
Impacto
Una dosis de insulina de 200U rápida (4x el máximo) pasa con un toast de advertencia que el médico puede ignorar. La función devuelve true siempre. Sin logging del override. Riesgo de hipoglucemia grave o muerte por sobredosis.
Corrección requerida
Alertas críticas deben ser modales no dismissables que requieran confirmación explícita. El override debe registrarse con usuario, timestamp y motivo.
if (dose > max) { return { allowed: false, reason: 'DOSE_EXCEEDED', requiresOverride: true, overrideFields: ['reason', 'clinician_id'] }; }
CRÍTICO PHI Datos completos de pacientes en logs de servidor (JSON.stringify)
Impacto
La búsqueda de pacientes loguea nombre completo, NIF y fechas de nacimiento con JSON.stringify(patients.data). En un volumen de 85.000 pacientes, cada búsqueda filtra datos a los logs del servidor. Constituye brecha de datos bajo RGPD Art. 33 — notificación obligatoria a AEPD en <72h.
Corrección requerida
Eliminar todo log de PHI. Usar solo contadores o identificadores técnicos (hash, count) en logs.
// CORRECTO: log sin PHI console.log(`Patient search: results=${patients.data?.length ?? 0}`);
ALTO PHI Sin Row-Level Security en todas las tablas clínicas
Impacto
Sin políticas RLS en Supabase, cualquier usuario autenticado puede leer registros de pacientes de otras clínicas o centros. Cross-facility data isolation no verificado. Viola segregación de datos obligatoria en entornos multi-tenant sanitarios.
Corrección requerida
Habilitar RLS en todas las tablas con datos de paciente y crear políticas que filtren por facility_id o provider_id del JWT.
ALTER TABLE patients ENABLE ROW LEVEL SECURITY; CREATE POLICY "facility_isolation" ON patients USING (facility_id = auth.jwt()->>'facility_id');
ALTO CLINICAL ON DELETE CASCADE elimina historia clínica al borrar paciente
Impacto
Las tablas medications y encounters tienen ON DELETE CASCADE. Borrar un paciente (incluso accidentalmente) destruye toda su historia farmacológica y clínica irreversiblemente. Datos de salud legalmente requeridos pueden desaparecer.
Corrección requerida
Cambiar a soft-delete con campo deleted_at. Nunca CASCADE DELETE en registros clínicos.
-- Cambiar CASCADE por RESTRICT ALTER TABLE medications DROP CONSTRAINT medications_patient_id_fkey, ADD CONSTRAINT medications_patient_id_fkey FOREIGN KEY (patient_id) REFERENCES patients(id) ON DELETE RESTRICT;
ALTO CLINICAL Edición directa de encuentros — sin addendum, sin audit trail
Impacto
El endpoint PUT /encounters sobrescribe las notas clínicas directamente sin registrar el cambio. En medicina, la historia clínica debe ser append-only con addendum. La ausencia de audit trail imposibilita auditorías y es incumplimiento RGPD Art. 32.
Corrección requerida
Implementar modelo addendum: insertar nueva entrada con referencia al original + razón de corrección, nunca UPDATE directo.
// Crear addendum, no editar encuentro original await db.query( 'INSERT INTO encounter_addenda (encounter_id, clinician_id, notes, reason) VALUES ($1,$2,$3,$4)', [encounterId, req.user.id, req.body.notes, req.body.reason]);
ALTO TECHNICAL SQL injection en checkInteractions (interpolación directa de patientId)
Impacto
La query WHERE patient_id = ${'{'}patientId{'}'} interpola directamente el ID sin parametrizar. Un patientId malicioso puede exfiltrar datos de toda la base de datos o destruirla.
Corrección requerida
Usar siempre consultas parametrizadas como en el resto del código.
// CORRECTO: parametrizado const meds = await db.query( 'SELECT * FROM medications WHERE patient_id = $1', [patientId]);
MEDIO CLINICAL NEWS2: puntuación SpO2 92–93% incorrecta (score 2 en lugar de 1, Escala 1 RCP)
Impacto
El umbral SpO2 92–93% devuelve score 2 pero la especificación NEWS2 del Royal College of Physicians asigna score 1 para ese rango en Escala 1. Podría clasificar pacientes erróneamente como HIGH risk activando respuestas clínicas innecesarias.
Corrección requerida
Verificar contra tabla publicada RCP NEWS2 (2017). SpO2 92–93% = +1. SpO2 ≤91% = +3. SpO2 94–95% = +1. SpO2 ≥96% = 0.
// Corrección según RCP NEWS2 Escala 1 if (vitals.spo2 <= 91) score += 3; else if (vitals.spo2 <= 93) score += 2; // ← verificar else if (vitals.spo2 <= 95) score += 1; else score += 0; // ≥96%
Resumen de Precisión Clínica
Umbrales 35.0/36.0/38.0/39.0 correctos
Componente Estado Spec de referencia Observación
Drug interaction checker FALLA (false negatives) Base de datos propia Fallo silencioso — bloquear despliegue
Validación dosis insulina (tipos conocidos) PARCIAL (no bloquea) Vademécum local + ficha técnica Detecta exceso pero no impide prescripción
NEWS2 — Frecuencia respiratoria CORRECTO RCP NEWS2 2017 Umbrales 8/11/20/24 correctos
NEWS2 — SpO2 Escala 1 REVISAR RCP NEWS2 2017 Rango 92–93% posiblemente sobreestimado +1 punto
NEWS2 — Temperatura CORRECTO RCP NEWS2 2017
NEWS2 — AVPU CORRECTO RCP NEWS2 2017 Penalización +3 si no Alert — correcto
NEWS2 — Risk stratification CORRECTO RCP NEWS2 2017 Umbrales ≥7 HIGH, ≥5 MEDIUM correctos
ICD-10 / SNOMED mappings NO REVISADO CIE-10-ES (MSSSI) No incluido en fragmentos enviados — revisar en siguiente iteración