Security Blog
Posted By Gregory

Under the Hood of WP Cerber 9.9


English version: Under the Hood of WP Cerber 9.9


Esta actualización se centra principalmente en aspectos de WP Cerber que pasan desapercibidos hasta que surge un problema. Nos hemos dedicado a reforzar la forma en que se almacenan y recuperan las configuraciones, cómo el Inspector de Tráfico lee el JavaScript ofuscado y cómo se comporta el plugin en plataformas de alojamiento antiguas. También hemos finalizado una migración que llevaba tiempo en marcha desde la API de configuración de WordPress. A continuación, te explicamos los cambios internos y qué significan para los sitios web que administras.

Configuración que se conserva tras una base de datos corrupta

WP Cerber guarda su configuración en una única opción almacenada, CERBER_CONFIG . Hasta ahora, si ese valor se corrompía y ya no se podía deserializar, el plugin pasaba el resultado dañado directamente a array_merge() . En PHP 8, esto generaba un TypeError fatal al cargar el plugin. Como el fallo ocurría durante la carga, provocaba la caída de todo el sitio, no solo de las pantallas de administración del plugin.

Ahora, crb_get_settings() valida el valor que devuelve crb_unserialize() antes de utilizarlo. Si los datos almacenados no se pueden analizar en una matriz, el complemento recurre a su configuración predeterminada en lugar de fallar. Registra el fallo como un problema crítico persistente mediante CRB_Issues::add() en el código ` corrupted_settings . Este problema se resuelve automáticamente la próxima vez que guarde la configuración, lo cual se ejecuta dentro de cerber_settings_update() .

Tuvimos cuidado de diferenciar dos casos que se parecen pero no lo son. Un valor almacenado vacío o erróneo simplemente significa que la configuración aún no existe. Ese valor nunca se deserializa y el complemento vuelve a la configuración predeterminada sin generar ningún problema. Informarlo sería una falsa alarma. El problema corrupted_settings ahora solo se activa cuando un valor almacenado no vacío no se deserializa correctamente en una matriz, que es la condición exacta que provocó el fallo original en producción.

Dos ramas cercanas en la misma función recibieron la misma disciplina. La fusión de compatibilidad del complemento Cloudflare ahora requiere una matriz antes de fusionarse. La rama CERBER_WP_OPTIONS siempre devuelve una matriz cuando no se solicita ninguna configuración específica.

Un respaldo autorreparable en la parte superior del protector.

Si bien recurrir a la configuración predeterminada mantiene el sitio en funcionamiento, también descarta la configuración del administrador. Por ello, hemos añadido una capa de recuperación por encima de la protección. Una nueva clase, CRB_Settings_Backup , guarda una copia válida de CERBER_CONFIG en el almacenamiento de clave-valor del propio complemento.

La copia de seguridad se almacena como JSON sin formato junto con el ID del usuario que la creó, una marca de tiempo y el contexto que la generó. Se actualiza tras una actualización de configuración exitosa, tras una importación de configuración, tras una actualización de un complemento y mediante la tarea de mantenimiento diaria. Solo una configuración válida y un código de contexto conocido pueden reemplazar una copia de seguridad existente.

Cuando el detector de corrupción encuentra un CERBER_CONFIG ilegible, la recuperación se ejecuta automáticamente. Tras una restauración exitosa, el complemento registra una advertencia que se puede descartar, la cual explica lo sucedido y solicita que revise y guarde la configuración. Esta advertencia desaparece una vez que lo haga. Si no existe una copia de seguridad utilizable o no se puede escribir el valor restaurado, el complemento mantiene su comportamiento de último recurso y vuelve a la configuración predeterminada. En ese caso, registra un problema crítico en lugar de la advertencia.

WP Cerber settings recovery flow. Missing settings quietly fall back to defaults, while corrupted settings trigger automatic recovery from the last-known-valid backup.

WP Cerber settings recovery flow. Missing settings quietly fall back to defaults, while corrupted settings trigger automatic recovery from the last-known-valid backup.

Para lograr una recuperación correcta, era necesario respetar el almacenamiento en caché de WordPress. recover() elimina la opción ` CERBER_CONFIG dañada antes de escribir el valor restaurado. Sin este paso, una caché de opciones obsoleta podría hacer que update_site_option() tratara el valor como si no hubiera cambiado y omitiera la escritura en la base de datos. La copia de seguridad se lee y se escribe sin tener en cuenta la caché de objetos, por lo que solo se confía en el registro persistente cerber_sets . El conjunto de códigos de contexto aceptados es cerrado. Tanto el escritor, sync() , como el validador de carga útil solo aceptan los cuatro códigos declarados, por lo que cada carga útil almacenada puede pasar la misma validación que realiza la recuperación posteriormente.

No más errores fatales en hosts sin mysqlnd

WP Cerber lee la base de datos mediante mysqli. Una ruta de recuperación, CRB_Database::fetch_result_set() , utiliza mysqli_result::fetch_all() para obtener un conjunto completo de resultados en una sola llamada. Este método solo existe cuando la extensión mysqli de PHP se compila con el controlador mysqlnd. En los sistemas donde mysqli se compila con la biblioteca libmysqlclient (anterior), el método no está disponible y su llamada produce un error fatal.

La solución sigue un patrón ya utilizado en otras partes de cerber-common.php . Antes de ejecutar la ruta rápida, el código ahora verifica function_exists('mysqli_fetch_all') . Si esta función no está disponible, lee el conjunto de resultados fila por fila con mysqli_result::fetch_array() , tanto para las formas MYSQLI_ASSOC como MYSQLI_NUM . Se conservan el orden de las filas, la estructura de los resultados, la limpieza y el contrato de retorno Revalt existente. La alternativa devuelve los mismos datos que la ruta rápida.

También proporcionamos a los administradores una forma de saber cuándo se activa el modo de reserva. Un nuevo detector en CRB_Issue_Monitor genera una advertencia, db_driver_no_mysqlnd , cuando la extensión mysqli está cargada pero falta mysqli_fetch_all() . El aviso aparece en el widget de Preparación del sistema. Es informativo. Confirma que el complemento sigue funcionando mediante el modo de reserva y recomienda habilitar mysqlnd para una mejor compatibilidad y rendimiento.

Detección más precisa de JavaScript ofuscado

CRB_JS_Detector inspecciona los campos de solicitud como parte del Inspector de Tráfico, que está habilitado por defecto. Busca código JavaScript ofuscado para ocultar primitivas como eval , script y XMLHttpRequest . El objetivo del diseño es una detección de alta confianza con una baja tasa de falsos positivos. El detector solo decodifica la entrada que es claramente una cadena codificada y deja todo lo demás intacto. Varias modificaciones en esta versión corrigieron una vulnerabilidad real y ampliaron el alcance de la detección.

Una regresión que permitía el paso de cadenas con escape hexadecimal completo.

La versión comenzó con una corrección de errores. La heurística de escape hexadecimal normalizaba cada cadena coincidente con trim( $m, "\\'\"" ) . La trim() de PHP trata su segundo argumento como un conjunto de caracteres a eliminar, no como un prefijo literal. La barra invertida en ese conjunto eliminaba la barra invertida inicial del primer escape \xNN junto con la comilla de apertura. Lo que quedaba tenía una longitud impar de 2N + 1 Un mecanismo de protección de longitud impar decidía entonces que la cadena no estaba codificada correctamente en hexadecimal y omitía la decodificación por completo. El resultado práctico era que una cadena construida únicamente con escapes \xNN pasaba desapercibida, y las primitivas con escape hexadecimal como eval , script y XMLHttpRequest no se detectaban en la ruta de campo de solicitud predeterminada. La corrección restaura la decodificación correcta de las cadenas con escape hexadecimal completo.

Mayor cobertura de códigos de escape y de caracteres.

Luego ampliamos lo que entiende el detector. La heurística de escape solía reconocer solo \xNN . Ahora también maneja escapes \uNNNN y \u{...} , incluyendo cadenas que mezclan estos formatos, manteniendo la regla de que solo se inspeccionan las cadenas completamente escapadas. La decodificación se trasladó a una función auxiliar dedicada, cerber_decode_js_escapes() , que decodifica los puntos de código ASCII y conserva todo lo demás. Si ocurre un error interno de PCRE, esa función auxiliar devuelve la entrada original en lugar de una cadena vacía, por lo que un fallo de decodificación no puede borrar el valor que se está comprobando.

La heurística de código de caracteres también cambió. Anteriormente, solo analizaba la salida decodificada en busca de URL externas y direcciones IP. Ahora también busca primitivas de ejecución y DOM. Decodifica números únicamente a partir de una construcción explícita fromCharCode(...) en lugar de cualquier matriz numérica que encuentre. Ambas heurísticas comparten ahora un patrón de primitiva sensible a tokens con límites de identificador. Este cambio impide que palabras comunes como description y evaluation coincidan con las subcadenas eval o script que contienen.

Literales enteros envueltos

fromCharCode está definido para aplicar ToUint16 a cada argumento. Esto significa que un código ASCII más cualquier múltiplo de 65536 produce el mismo carácter. Una versión anterior de la heurística solo aceptaba literales de hasta seis dígitos, por lo que un valor de siete dígitos envuelto pasaba desapercibido. Por ejemplo, 1048677 se decodifica a la unidad de código 101, la letra e . El detector ahora acepta literales decimales y hexadecimales sin signo de hasta Number.MAX_SAFE_INTEGER y reduce cada uno a su unidad de código ToUint16 independientemente del tamaño entero de PHP. Rechaza los decimales con cero inicial, porque son ambiguos con el octal heredado. Limita el trabajo por dígito para que un literal muy largo en una solicitud pública no pueda generar un cálculo ilimitado.

Una actualización posterior solucionó una brecha relacionada. Devolver una cadena vacía en el primer argumento no compatible anuló toda la llamada decodificada. Un atacante podría agregar un único token fuera de rango a una carga útil que de otro modo sería detectable y suprimir la detección. Un ejemplo de dicho token es 9007199254741024 , que JavaScript asigna a un espacio mediante ToUint16. Ahora, un token estructuralmente válido pero no compatible se reemplaza con un guion bajo como centinela, y el resto de la llamada aún se decodifica. El guion bajo es un carácter de palabra, por lo que también impide que se forme un límite de identificador fabricado junto a una palabra clave.

Argumentos separados por comentarios

La última clase de evasión utilizaba comentarios de JavaScript. La heurística compactaba su entrada eliminando los espacios en blanco, lo que dejaba los comentarios en su lugar. Un comentario insertado entre argumentos numéricos rompía la coincidencia de la lista numérica, por lo que una llamada como String.fromCharCode(101,/*x*/118,97,108,40,49,41,59) evitaba la detección. El detector ahora elimina los comentarios de bloque y de línea de la lista de argumentos fromCharCode antes de validarla y decodificarla, mientras que conserva el contenido de las cadenas entre comillas. La captura también se ejecuta en modo dotall, por lo que una lista de argumentos que abarca varias líneas se lee como una sola llamada.

Retirada de la API de configuración de WordPress

Las páginas de configuración de WP Cerber se basaban en la API de configuración de WordPress. Los formularios se enviaban a /wp-admin/options.php , y el plugin utilizaba register_setting() , add_settings_section() , add_settings_field() , settings_fields() y do_settings_sections() para registrarlos y renderizarlos. Esto funcionaba, pero vinculaba la interfaz de administración del plugin a un subsistema procedimental de WordPress y sus convenciones. Esta versión completa la transición a un motor de formularios propio del plugin, y lo hicimos por etapas en lugar de como un único cambio.

Primero, definimos un límite. Todas las llamadas directas a la API de Configuración se trasladaron a una clase estática, CRB_Legacy_Settings_Manager . Un análisis del código reveló que solo se utilizaban cinco de las diez funciones de la API de Configuración, distribuidas en seis puntos de llamada en dos archivos. Las otras cinco nunca se invocaron, por lo que la clase de límite no incluye métodos para ellas. Este paso preservó el comportamiento original. Los nombres de las opciones, los grupos de opciones, los identificadores de sección y campo, las funciones de devolución de llamada, la sincronización de los hooks y la salida permanecieron inalterados.

Luego creamos el reemplazo. Una nueva clase, CRB_Settings_Renderer , renderiza secciones y filas de campos directamente desde la configuración declarativa devuelta por cerber_settings_config() . Los formularios de configuración ahora envían los datos a la página de administración del plugin en lugar de a options.php . Los envíos se procesan mediante la canalización existente en admin_init y luego se realiza una redirección POST-GET de vuelta a la página de configuración. El marcado renderizado es idéntico byte a byte al que producía do_settings_sections() anteriormente, incluyendo los encabezados, los bloques de sección y las filas de la tabla del formulario.

La verificación de nonce cambió con esta actualización. En lugar de usar check_admin_referer() contra un grupo de opciones de la API de configuración, el plugin ahora verifica su propio campo cerber_nonce . Este campo ya estaba presente en todos los formularios de configuración, incluidos los generados por versiones anteriores. Un nonce no válido o caducado ya no detiene la solicitud con wp_die() . En su lugar, se muestra un aviso de administrador y se regresa al formulario.

Al eliminar options.php también se suprimió una práctica obsoleta. El método anterior escribía una copia sin formato de cada grupo de formularios en una opción específica para cada grupo llamada cerber-{group} . Estas copias solo se leían durante la migración previa a la versión 9.3.4 y durante la limpieza tras la desinstalación, nunca en tiempo de ejecución. Ahora ya no se escriben.

Cerber.Hub y sitios gestionados

Dos contextos mantuvieron la ruta anterior durante un tiempo, ya que ambos dependían del formato de comunicación de la API de configuración. Uno era la representación remota de Cerber.Hub, donde el protocolo entre un sitio principal y sus sitios gestionados transmitía los campos option_page y _wpnonce . El otro era la pantalla de edición del sitio gestionado, que guardaba la configuración mediante un filtro pre_update_option activado por options.php . Migramos cada uno por separado.

El formulario de edición del sitio administrado ahora se renderiza a través de CRB_Settings_Renderer y se guarda mediante una nueva función, nexus_save_client_data_form() . Esta función reproduce exactamente el manejo de datos de la antigua función de devolución de llamada, incluyendo la resolución de grupos, la eliminación de detalles del propietario strip_tags y la limpieza de grupos no utilizados. Mantiene el mismo límite de seguridad con una protección explícita nexus_is_main() e is_super_admin() . El cliente Cerber.Hub también dejó de emular su API de configuración. Posteriormente, se eliminaron CRB_Legacy_Settings_Manager y varias funciones auxiliares que ya no se utilizan.

Los formularios de configuración remota ya no emiten los campos ocultos de la API de configuración como option_page , action=update , _wpnonce o _wp_http_referer . Ahora llevan los mismos campos internos que los formularios locales, más el sello Nexus en el contexto remoto. La autenticación de transporte Nexus no ha cambiado y sigue siendo el límite externo para las solicitudes de sitios administrados. nexus_is_valid_request() , nexus_is_granted() y el viaje de ida y vuelta cerber_nexus_seal funcionan como antes. Un envío reenviado todavía verifica el nonce del plugin antes de que se ejecute cualquier procesamiento de configuración. También agregamos una verificación del punto de entrada. Una pantalla de configuración enviada que no se resuelve a una pantalla conocida ahora falla cerrada con un WP_Error antes de que comience el procesamiento, y el mensaje de error incluye el valor enviado para diagnósticos.

Una transición difícil, y algo que debes saber después de la actualización.

Se trató de una transición drástica y deliberada. No mantuvimos una ruta de transición de doble formato, y ya no se procesa options.php . Esta decisión conlleva un caso excepcional que conviene aclarar. Si un formulario de configuración se generó con la versión anterior de WP Cerber y lo envía después de la actualización, es posible que no se guarde esa primera vez. En ese caso, vuelva a abrir la página de configuración y envíelo de nuevo. El formulario recién generado incorpora el nuevo contrato interno y se guarda correctamente. Lo mismo se aplica a los formularios de configuración remotos en sitios administrados.

La migración incluyó una limpieza de nombres. El término sobrecargado group se renombró a settings_screen_id en todos los lugares donde identificaba una pantalla de configuración, incluyendo la constante, el valor del campo de formulario oculto, las firmas de la función de canalización y las claves de configuración. El código de error remoto se renombró de unknown_settings_group a unknown_settings_screen . Un detalle de compatibilidad es importante para los autores de complementos. La carga útil del evento update_settings aún conserva la clave group antigua como un alias de settings_screen_id , porque cerber_add_handler() es pública y los manejadores externos pueden leerla.

Trabajos preliminares para la fábrica de interfaz de usuario

En las pantallas de administración, WP Cerber renderiza HTML mediante una fábrica de interfaz de usuario interna en lugar de marcado en línea. Esta versión añadió dos bloques estructurales básicos. crb_ui_fragment() produce una colección mixta y ordenada de elementos secundarios sin etiqueta contenedora. crb_ui_element_set() hace lo mismo para una colección homogénea con un tipo de elemento secundario definido. Renombramos el generador de fragmentos fluido CRB_UI_Fragment_Builder a CRB_UI_Content_Builder para evitar confusiones con el nuevo nodo de fragmentos, y mantuvimos un alias de clase para que el código existente siga funcionando.

Como resultado, algunos sitios de llamadas se simplificaron. crb_ui_message_box() ahora acepta cadenas y números sin formato y los envuelve en elementos de párrafo con caracteres de escape, por lo que los usuarios ya no tienen que escribir esos párrafos manualmente. El aviso de error de obtención duplicado en el panel de control y la pantalla de registro de tráfico se trasladaron a una única función auxiliar compartida. El panel de diagnóstico del entorno se reconstruyó en los nuevos nodos. En todos los casos, el HTML renderizado permanece sin cambios. Esto sienta las bases para una interfaz de administración independiente del motor de renderizado, y actualmente no es visible en la página.

Después de actualizar

Una breve lista de verificación para administradores:

  • Si un formulario de configuración no se guarda al enviarlo por primera vez justo después de la actualización, vuelva a abrir la página de configuración y guárdelo de nuevo.
  • Si WP Cerber vuelve a la configuración predeterminada o la restaura desde una copia de seguridad, te lo notificará mediante un aviso en el panel de administración. Revisa tu configuración y guárdala para eliminar la notificación.
  • Si tu servidor ejecuta PHP sin el controlador mysqlnd, consulta el aviso en el widget de Preparación del sistema. El plugin seguirá funcionando y el aviso explicará el cambio recomendado.

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.