Casos de uso
Casos de auditoría: qué parecía, qué era y cómo lo comprobé
Cada uno cuenta lo mismo: qué medí, qué parecía al principio, por qué lo descarté y qué era en realidad. Van anonimizados por tipo de organización, con las cifras que se midieron.
Caso
Miles de procesos regeneraban a la vez los mismos objetos de caché
Durante una prueba de carga, el clúster caía en el primer minuto y medio y devolvía errores 500 y 502. Medí la latencia en el balanceador, las conexiones activas a Redis y la E/S de disco en PostgreSQL. Al principio pensé en falta de escalado en la base de datos o en los nodos de PHP-FPM, hasta que vi que la saturación coincidía con miles de consultas idénticas y con el agotamiento de conexiones a Redis. Era una estampida de regeneración: miles de claves de la caché de aplicación expiraban a la vez y cientos de procesos PHP reconstruían los mismos objetos de contexto y de plugins. Separé las sesiones y la caché de aplicación en almacenes de Redis independientes, mantuve la caché de petición dentro de cada proceso, escaloné las caducidades y añadí un bloqueo por clave con actualización en segundo plano: mientras un proceso regenera, los demás reciben temporalmente la copia caducada. Activé además conexiones persistentes con serialización binaria para evitar una nueva conexión TCP en cada petición. En el recorrido y la duración documentados en el informe confidencial, la plataforma sostuvo 15.000 sesiones concurrentes. Las cifras de latencia y uso de CPU pertenecen a ese escenario y no constituyen una capacidad general de la plataforma.
Caso
El autoguardado concentraba las escrituras de 25.000 cuestionarios en el mismo minuto
En una convocatoria con decenas de miles de cuestionarios a la vez, los intentos y las respuestas se bloqueaban, los interbloqueos se repetían y el rendimiento se desplomaba a mitad de examen. Medí las consultas lentas, los hilos en espera de bloqueo, la memoria de la base de datos y la frecuencia de autoguardado. Lo primero que me pidieron fue una instancia con más CPU y memoria. Los datos decían otra cosa: la carga se concentraba en escrituras síncronas periódicas y en la competencia entre lecturas y escrituras sobre el mismo nodo. El origen era el autoguardado de respuestas, que se ejecutaba cada minuto mientras las consultas de intentos y de navegación seguían leyendo del nodo de escritura. Ajusté el intervalo de autoguardado y las peticiones de finalización. Desvié a réplicas únicamente las lecturas que toleraban retraso y mantuve en el nodo principal las que debían reflejar de inmediato una escritura. También afiné max_connections, shared_buffers, work_mem y el autovacuum. En la repetición documentada desaparecieron los interbloqueos y se alcanzó la concurrencia prevista; las métricas detalladas pertenecen al informe confidencial del caso.
Caso
Una consulta sin índice recorría doce millones de filas cada vez que se mostraba un bloque
Después de una migración de versión, abrir la página principal de un curso pasó de 400 ms a más de 6 segundos bajo carga, en una plataforma con años de extensiones acumuladas. Perfilé las peticiones con instrumentación de PHP, conté las consultas de cada una y medí su duración. Sospeché primero de la memoria de los nodos PHP y de la acumulación histórica en la base de datos. El perfilado lo desmintió al aislar una sola consulta cuyo plan recorría la tabla entera. Venía de una tabla propia de un plugin de seguimiento: doce millones de filas y ningún índice compuesto por curso y usuario. Activé el registro de consultas lentas, analicé el plan de ejecución, creé el índice que faltaba y reescribí el código para sustituir las consultas dentro del bucle por una sola consulta agregada. El tiempo bajó de 6 s a 250 ms y las lecturas de disco se redujeron un 85 %.
Caso
Los informes expulsaban de memoria los datos operativos al recorrer más de ochenta millones de registros
En un Centro de Formación Profesional, la plataforma se congelaba cada mañana cuando los coordinadores generaban los informes de seguimiento y los estudiantes se quedaban colgados. Medí las consultas lentas, los aciertos de caché de consultas y los bloqueos de metadatos. Parecía que el servidor se quedaba sin procesos PHP libres por un pico imprevisto de alumnos, y lo descarté porque los bloqueos coincidían con consultas por fecha y contexto sobre el registro de actividad. La tabla tenía más de ochenta millones de filas y no estaba particionada. Las consultas de los tutores ocupaban memoria de la base de datos y expulsaban del área de trabajo las páginas que sí utilizaba la actividad diaria. Separé los registros hacia un almacén de solo lectura, dejé de guardar eventos que nadie consultaba, particioné por rangos de fecha y programé el archivado de lo que tuviera más de doce meses. Se acabaron las congelaciones durante los informes y la base operativa se redujo más de un 60 %.
Caso
Borrar un 40 % de los datos no redujo las páginas que la base de datos seguía leyendo
En una institución educativa, la base de datos respondía más despacio después de purgar 50.000 usuarios inactivos y reiniciar 3.000 cursos, aunque contenía un 40 % menos de datos. Medí el tamaño físico en disco, la fragmentación de los índices y las lecturas de página por consulta. Parecía corrupción de la base de datos o un fallo del almacenamiento, y lo descarté porque los ficheros conservaban prácticamente el mismo tamaño y las consultas seguían recorriendo páginas vacías. Los borrados masivos habían dejado huecos en las páginas de tablas e índices, pero el motor mantenía ese espacio fragmentado. Escribí una comprobación para localizar las tablas con más de un 30 % de espacio libre, reconstruí tablas e índices durante una ventana de mantenimiento y ajusté el factor de llenado para las tablas con muchas lecturas y pocas actualizaciones. Recuperé más de 150 GB de disco y reduje un 35 % el tiempo de las consultas que leían por índice.
Caso
El servicio confundía autenticación con autorización y exponía los datos de todo el alumnado
Durante una revisión de seguridad detecté que cualquier usuario autenticado podía leer datos personales y calificaciones de otros a través de un servicio propio. Lo confirmé combinando el análisis estático del control de acceso con pruebas dinámicas sobre ese servicio. Parecía que la autenticación bastaba para proteger el acceso, y descarté esa idea: estar autenticado no es estar autorizado. El servicio consultaba la base de datos sin comprobar la capacidad correspondiente ni validar el contexto. Lo reescribí con la API oficial de servicios externos de Moodle, forcé la validación del identificador contra el contexto del curso y del sistema, y limité la respuesta a los atributos necesarios. Añadí también reglas en el cortafuegos de aplicación para detectar recorridos secuenciales de identificadores. El vector quedó cerrado y la reevaluación superó la prueba de intrusión.
Caso
La conformidad exigía corregir barreras que el escaneo automático no detectaba
La plataforma tenía que cumplir los requisitos de accesibilidad definidos para el encargo y la revisión inicial encontró barreras. Combiné el escaneo automático con una revisión manual, con lectores de pantalla y navegando solo con el teclado. Así vi que los cuestionarios atrapaban el foco, el tema corporativo no llegaba al contraste mínimo y varios componentes de terceros no funcionaban con lectores de pantalla. Sobre el papel podía parecer suficiente cumplir WCAG 2.1 en nivel AA, pero la norma europea EN 301 549, en su versión armonizada 3.2.1, añade requisitos propios. Es el marco que exige en España el Real Decreto 1112/2018. El uso real destapó problemas que el escaneo no encontraba. Reescribí el CSS y las plantillas para corregir el contraste, añadí los atributos que anuncian los cambios de estado en menús, acordeones y ventanas modales, y recuperé el indicador de foco que alguien había suprimido. Resolví además la trampa de foco de las preguntas de arrastrar y soltar con una alternativa por teclado. En la versión, la muestra y los criterios evaluados, la revisión final no dejó hallazgos abiertos en la interfaz base ni en los tipos de cuestionario incluidos.
Caso
El ataque entraba saneado como HTML y se ejecutaba después desde una tarea programada
Un escaneo rutinario del cortafuegos de aplicación detectó cargas sospechosas dirigidas a las tareas en segundo plano de un desarrollo propio. Parecía un intento convencional, ya neutralizado al sanear el formulario. Lo descarté siguiendo el recorrido de los datos no fiables y contrastándolo con el registro general de consultas: un usuario sin autenticar podía acabar ejecutando código en producción. Era una inyección de SQL de segundo orden. Los fragmentos entraban por campos de texto, se guardaban saneados como HTML y más tarde el plugin los concatenaba sin parametrizar en una consulta que movía datos entre tablas. Desde ahí se llegaba al sistema de ficheros con sentencias múltiples o funciones privilegiadas. Sustituí las consultas concatenadas por la API de base de datos de Moodle con parámetros con nombre y añadí a la integración continua una regla que rechazaba cualquier concatenación de variables en llamadas a la base de datos. Eliminé el vector y la auditoría externa terminó sin hallazgos críticos.
Caso
El estado de una encuesta permitía encadenar clases hasta ejecutar comandos
Durante una auditoría interna detecté que alguien con rol de profesor podía ejecutar comandos en el sistema operativo a través de una extensión sin mantenimiento. Parecía un problema de permisos del rol, pero el análisis estático del código destapó una deserialización insegura. La extensión recibía una cadena preparada en un campo del formulario o en una cookie de sesión y la deserializaba directamente, lo que permitía encadenar clases ya cargadas en memoria hasta ejecutar código. Sustituí la serialización nativa de objetos por JSON, con lo que la ejecución dejó de ser posible. Donde los registros antiguos impedían migrar de inmediato, restringí la deserialización bloqueando la instanciación de clases. Eliminé el vector y la extensión quedó conforme al estándar de desarrollo seguro de la plataforma.