Security Blog
Posted By Gregory

Under The Hood 9.8.3


English version: Under The Hood 9.8.3


Esta versión es más un ciclo de mantenimiento y fortalecimiento que un lanzamiento de nuevas funciones. La mayor parte del trabajo se centró en aspectos que los administradores de sitios rara vez revisan: la capa de datos que lee los registros de seguridad, el código que genera los correos electrónicos de notificación y las rutas de exportación que proporcionan un archivo CSV con los registros de actividad. Si bien nada de esto modifica la apariencia del complemento, sí mejora su fiabilidad y seguridad cuando un sitio tiene mucho tráfico, cuando el registro es extenso o cuando un atacante intenta explotar una vulnerabilidad que aún no habíamos resuelto.

A continuación se presenta un resumen a nivel de implementación para administradores y desarrolladores que deseen comprender qué fue lo que realmente cambió.

Cierre de un vector de inyección de destinatario de correo electrónico

WP Cerber envía dos tipos de correo electrónico transaccional que incluyen el nombre de la persona: el mensaje PIN de autenticación de dos factores y la notificación de alerta de actividad. Ambos construyen el destinatario en el formato familiar RFC 5322 Name <email> , y ambos toman ese nombre directamente de los campos del perfil de WordPress ( user_firstname , user_lastname y display_name ) sin escape.

El problema radica en cómo wp_mail() gestiona el destinatario de una cadena. Divide la cadena por una coma literal y no respeta los límites de las comillas. Un usuario que haya incluido los caracteres adecuados en su nombre de perfil podría, por lo tanto, insertar una dirección adicional en la lista de destinatarios del correo electrónico con el PIN de la autenticación de dos factores (2FA) generado por CRB_2FA::send_user_pin() y del correo electrónico de alerta generado mediante la rama user_list de cerber_get_email() . En un sitio donde cualquiera puede registrarse y establecer un nombre para mostrar, esto representa una vía real para redirigir silenciosamente una copia de un mensaje de seguridad.

Hemos añadido un sanitizador específico, crb_sanitize_mail_display_name() , en cerber-common.php y lo hemos aplicado en ambos puntos de llamada. Este sanitizador elimina precisamente los caracteres relevantes para este tipo de inyección: la coma, las comillas, la barra invertida, los corchetes angulares y los caracteres de control. El nombre visible se sigue mostrando con normalidad para los usuarios legítimos, y la lista de destinatarios ya no puede ser manipulada mediante un campo de perfil malicioso.

Eliminación de un vector XSS almacenado en el escáner de propiedad.

El escáner de malware informa cuando cambia la propiedad de un plugin instalado en el repositorio de WordPress.org. Para ello, utiliza los metadatos de propiedad proporcionados por dicho repositorio y, hasta ahora, integraba parte de esos metadatos en el aviso de administración como código HTML sin formato, incluyendo etiquetas de anclaje creadas manualmente para los enlaces del perfil del propietario.

Ese repositorio es una fuente confiable y la probabilidad de metadatos maliciosos es baja, pero los datos externos nunca deberían llegar a la interfaz de administración como código HTML sin codificar. Considerarlo una fuente confiable es precisamente la suposición en la que preferimos no confiar.

Para cerrar el vector correctamente en lugar de parchearlo localmente, introdujimos un nuevo elemento de UI Factory, formatted_text , construido mediante la función auxiliar crb_ui_formatted_text() . Este elemento renderiza una plantilla de texto plano que contiene marcadores de posición numerados %N$s . Los fragmentos literales de la plantilla y los argumentos escalares se escapan en HTML, mientras que cualquier argumento que sea en sí mismo un elemento de la interfaz de usuario se renderiza mediante el renderizador activo. Por lo tanto, cada valor dinámico se escapa en el contexto donde se emite realmente, que es la única forma fiable de evitar errores de contexto de salida.

El mensaje de propiedad en crb_check_ownership() ahora usa este elemento, y los enlaces del perfil del propietario se construyen con crb_ui_link() en lugar de cadenas de anclaje concatenadas, por lo que las URL de perfil y los nombres para mostrar se escapan como URL y como texto respectivamente. La cadena de traducción no ha cambiado, por lo que las localizaciones siguen funcionando. El riesgo de XSS de administrador almacenado ha desaparecido.

Solucionar el problema del filtro "Cualquier error de software" en el Inspector de tráfico.

En el registro del Inspector de Tráfico, el formulario de Búsqueda Avanzada permite combinar varias condiciones. Una de ellas es la casilla de verificación "Cualquier error de software". Al combinar esta casilla con otros filtros, las solicitudes con un error de PHP registrado podían aparecer en los resultados incluso si no cumplían con las demás condiciones especificadas.

La causa principal era la precedencia de operadores en la forma en que se construía la cláusula WHERE. El código anterior concatenaba los fragmentos del filtro como cadenas, y la condición de error agrupada no estaba entre paréntesis, por lo que un OR dentro de ella podía eludir la lógica AND circundante. Como parte de la migración de la consulta de tráfico al constructor de consultas, la condición de error agrupada ahora está correctamente entre paréntesis, y el filtro combinado se comporta como lo indica el formulario. Al restringir una búsqueda, los resultados ahora respetan todas las condiciones establecidas.

Coincidencia de comodines de búsqueda literalmente

Anteriormente, la búsqueda avanzada en el registro de tráfico permitía que un % o _ introducidos en un término de búsqueda actuaran como un metacaracter LIKE , ya que el término se insertaba en el patrón sin escape. Esto es, en el mejor de los casos, una característica accidental y, en el peor, un patrón de carga innecesario.

Ahora, cada término LIKE pasa por CRB_Database::escape_like() antes de que se añadan los comodines circundantes, por lo que % y _ se interpretan como los caracteres literales que el usuario ha escrito. Las búsquedas se comportan de forma predecible y la consulta no puede ser forzada a realizar un escaneo mayor del previsto.

Corrección del manejo del límite de memoria para las exportaciones

Exportar un registro de actividad o tráfico extenso consume mucha memoria, por lo que WP Cerber aumenta la memoria disponible antes de la operación. En algunos entornos, un valor numérico de límite de memoria, como 512 se interpretaba como bytes en lugar de megabytes. En esos casos, el plugin no aumentaba el límite como debía y la exportación podía detenerse antes de lo previsto.

Ahora el valor se interpreta con la unidad correcta, por lo que el aumento de memoria se aplica según lo previsto y las exportaciones grandes se completan en los entornos que antes se veían afectados.

Reconstrucción de exportaciones de registros en torno a la transmisión

Las exportaciones de Actividad y Tráfico solían leer las filas coincidentes en bloques, volviendo a ejecutar la misma SELECT con un OFFSET creciente para cada bloque. En un registro grande, esto se degrada a un escaneo de desplazamiento profundo, donde cada bloque cuesta más que el anterior y mantiene un resultado creciente en la memoria.

Ahora, ambas exportaciones leen sus filas en una sola pasada sin búfer a través de CRB_Database::query_stream() , envuelta en un generador que produce una fila a la vez. El uso de memoria se mantiene constante independientemente de la cantidad de registros coincidentes, y la base de datos realiza el trabajo una sola vez en lugar de una vez por bloque. Debido a que un flujo sin búfer bloquea la conexión mientras se consume, el total de filas y el rango de fechas se resuelven de antemano con consultas separadas COUNT y MIN / MAX con búfer, construidas a partir de los mismos filtros, antes de que se abra el flujo.

Se agregaron dos encabezados de respuesta al asistente de descarga de archivos compartidos, crb_file_headers() . X-Accel-Buffering: no le indica a Nginx, cuando se encuentra frente a PHP-FPM, que reenvíe cada fragmento inmediatamente en lugar de almacenar en búfer toda la exportación antes de enviarla, lo que mejora el tiempo hasta el primer byte y evita que el proxy mantenga un CSV grande en memoria. El encabezado es específico de Nginx y Apache con mod_php y otros proxies lo ignoran sin problemas. Cache-Control: no-store impide que el navegador o cualquier proxy intermedio almacene en caché una exportación de registro de seguridad confidencial.

También hicimos que la limpieza del flujo fuera determinista. La exportación consume el generador dentro de un bloque try / finally y libera el único identificador restante en finally , de modo que el resultado sin búfer se libera y la conexión se desbloquea en cada ruta de salida: finalización normal, una parada anticipada o una excepción lanzada mientras se escribe una fila. Anteriormente, una interrupción anticipada o un error antes del consumo completo podía dejar el resultado abierto y la conexión bloqueada, lo que provocaría que la siguiente consulta en esa solicitud fallara.

Informar sobre el rango de fechas exportadas

El encabezado de exportación CSV solía mostrar solo los filtros activos. Ahora, tanto la exportación de Actividad como la de Tráfico añaden dos filas al encabezado, que muestran las marcas de tiempo de los registros más antiguos y más recientes incluidos en los datos exportados. Dado que el encabezado se escribe antes de la primera fila, el rango proviene de una consulta MIN / MAX complementaria, creada a partir de la misma consulta filtrada que la exportación, y se omite cuando no hay filas coincidentes. Al archivar una exportación, el archivo ahora registra el intervalo de tiempo exacto que representa.

Hacer visibles los fracasos en las exportaciones en lugar de silenciarlos.

El antiguo método de exportación podía fallar silenciosamente. Si la base de datos no estaba disponible o no se podía abrir el flujo de filas, el código anterior solía generar un archivo CSV vacío sin ninguna explicación, lo cual es el peor escenario para alguien que intenta recuperar registros durante un incidente.

Se modificaron las rutas de lectura y exportación de ambos registros para que devuelvan un resultado Revalt que contenga la carga útil de datos o un error estructurado, con códigos distintos como activity_export_query_build_failed , activity_export_db_unavailable y activity_export_stream_failed . Un fallo de nivel inferior se encadena al resultado para que se conserve la causa raíz original en lugar de descartarla. Los fallos de configuración ahora finalizan la exportación con wp_die() antes de que se envíe un solo byte CSV, en lugar de transmitir un archivo vacío.

Cuando falla una exportación, un administrador con permisos de manage_options ve la causa raíz encadenada, por ejemplo, el error subyacente de la base de datos, adjunta al mensaje. Los usuarios sin esos permisos no la ven, por lo que la información relevante llega a quienes pueden actuar en consecuencia, mientras que los detalles internos de la base de datos permanecen ocultos para el resto. El mensaje se procesa mediante crb_escape_html() .

Consolidación de los registros de actividad y tráfico en clases de dominio

Gran parte de este ciclo era estructural. Tanto el registro de actividad como el de tráfico contenían cadenas SQL y el manejo de resultados dispersos en el código del panel de control y de exportación. Trasladamos esa lógica a las clases CRB_Activity y CRB_Traffic_Log , de modo que la creación de consultas y la obtención de filas ahora se realizan mediante métodos claros como fetch() y stream_log() en lugar de transmitirse a través del código de presentación como SQL sin procesar.

Ahora ambas clases construyen sus consultas con el constructor de consultas DB Warp obtenido mediante warp_get_db() en lugar de concatenar manualmente los fragmentos WHERE, JOIN y LIMIT. Esto va más allá de la pulcritud. Enrutar cada valor de filtro proporcionado por el usuario a través de una capa de escape elimina la mezcla anterior de escape manual, $wpdb->prepare() y concatenación entre comillas, que es precisamente el tipo de inconsistencia que oculta errores de inyección. Cuando una capa de base de datos degradada no puede construir una consulta, el código ahora falla de forma segura con una condición de no coincidencia en lugar de ejecutar una consulta sin filtrar.

Estas refactorizaciones son internas y no cambian lo que muestran las pantallas, pero son la base que hizo que las correcciones de seguridad y confiabilidad mencionadas anteriormente fueran pequeñas, locales y verificables.

Se encontraron dos errores latentes al extraer el código de alerta.

El traslado del envío de alertas de administración de CRB_Activity::log() a una clase dedicada CRB_Activity_Alerts sacó a la luz dos errores preexistentes causados por una matriz posicional cuyos índices numéricos se habían desalineado.

El primer problema afectó a los enlaces del panel de control dentro de los correos electrónicos de alerta. El emparejamiento de clave dispersa y valor denso alteró los valores, de modo que un parámetro de enlace como filter_ip podía recibir el inicio de un rango de IP en lugar de la dirección prevista. Al asignar los valores a claves con nombre, se corrigió la alineación y ahora los enlaces en los correos electrónicos de alerta apuntan a donde deberían.

El segundo problema afectaba la coincidencia de usuarios en la cadena de búsqueda. El código llamaba a wp_get_current_user() , que devolvía un objeto WP_User incluso para el ID de usuario 0, por lo que la coincidencia utilizaba la identidad incorrecta. Ahora busca al usuario del evento con crb_get_userdata() y evita que falte un usuario. Las alertas que coinciden con un usuario ahora coinciden con el correcto.

Un panel de control más silencioso: prioridad del operador en la verificación de modificaciones.

CRB_Activity::is_modified_since() comparaba una marca de tiempo usando $stamp < $status['data_modified'] ?? PHP_INT_MAX . La precedencia de operadores vincula eso como (... < ...) ?? PHP_INT_MAX , lo que hacía que la parte de coalescencia nula fuera código muerto y generaba un aviso de clave indefinida cuando data_modified estaba ausente. La coalescencia ahora está entre paréntesis, por lo que una marca de modificación faltante se trata como "modificada", coincidiendo con el método hermano is_modified() , y el aviso espurio desaparece.

Centralización de las definiciones de esquemas de bases de datos

La última pieza estructural es el mantenimiento del esquema. El código de instalación y actualización utilizaba SQL CREATE TABLE en línea para las tablas de registro, lo que significaba que la misma tabla podía describirse en más de un lugar. Esas declaraciones ahora provienen de una única fuente, CRB_Schema_Definitions , por lo que la instalación y la actualización se comportan a partir de una definición canónica y CRB_Schema_Manager puede detectar desviaciones de esquema en instalaciones existentes de manera más consistente. El SQL DROP INDEX sin procesar también se reemplazó con CRB_Schema_Manager::drop_index_if_exists() .

También hemos mejorado la tolerancia a errores en la decodificación de los datos almacenados en los campos de solicitud. Los valores heredados que admiten valores nulos, los valores vacíos, el JSON no válido y las cargas útiles serializadas no compatibles ahora se manejan de la misma manera segura, resolviendo en una matriz vacía en lugar de permitir que un registro mal formado interrumpa la lectura.

Por qué es importante este lanzamiento

En la versión 9.8.3 no hay botones nuevos. En su lugar, se ha implementado una capa de datos que gestiona los fallos de forma segura en lugar de silenciosa, notificaciones por correo electrónico que no se pueden manipular mediante un nombre de perfil personalizado, un aviso para el administrador que gestiona correctamente los datos externos, exportaciones de registros que se ejecutan en memoria plana e informan cuando algo falla, y filtros de búsqueda que funcionan exactamente como indica el formulario. Este tipo de cambios son los que garantizan la fiabilidad de un plugin de seguridad a largo plazo, y preferimos realizar este trabajo de forma transparente en lugar de pretender que el código anterior ya era perfecto.


I'm a team lead in Cerber Tech. I'm a software & database architect, WordPress - PHP - SQL - JavaScript developer. I started coding in 1993 on IBM System/370 (yeah, that was amazing days) and today software engineering at Cerber Tech is how I make my living. I've taught to have high standards for myself as well as using them in developing software solutions.

View Comments
There are currently no comments.