From ea8eb6dbcb221d3393239ed0e974685f0751d2d6 Mon Sep 17 00:00:00 2001 From: Jordan Diaz Date: Sat, 12 Sep 2026 22:51:56 +0000 Subject: [PATCH] fix(plugins): red de seguridad ante exit() en un gancho y globales del CMS Tras la revision de codigo de 273c3dd: - Un exit()/die() dentro de un gancho no es una excepcion y dejaba un cuerpo que no era JSON. register_shutdown_function en _pluginLifecycle responde {ok:false, error, messages:[lo impreso]} si el gancho no llego al final. - event/plugin/origin se castean solo si son escalares (un array producia un Warning delante del JSON). - Los ganchos ven CURRENT_USER, menu, action, TABLE_PREFIX y SETTINGS como un checkpoint desde addPlugins(); documentado en el contrato. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01HPL66q9F33YZxbLPUT7KW8 --- cms/lib/handlers/handler_ws.php | 25 +++++++++++++++++++++---- cms/lib/plugins/index.php | 8 +++++++- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/cms/lib/handlers/handler_ws.php b/cms/lib/handlers/handler_ws.php index 53d4ada..0cea669 100755 --- a/cms/lib/handlers/handler_ws.php +++ b/cms/lib/handlers/handler_ws.php @@ -412,15 +412,32 @@ function _pluginLifecycle() { // respeta ficheros personalizados y puede haber entregado este y no aquel. die(json_encode(["ok" => false, "error" => "El cargador de plugins de esta web no soporta ganchos (cms/lib/plugins/index.php desactualizado)"])); } - $event = (string) @$datos["event"]; - $plugin = (string) @$datos["plugin"]; + // Red de seguridad: exit()/die() dentro de un gancho no es una excepcion y + // se saltaria el die(json_encode(...)) de abajo dejando un cuerpo que no es + // JSON. Si al cerrar no hemos respondido, respondemos nosotros. + $GLOBALS["_pluginLifecycleRespondido"] = false; + register_shutdown_function(function () { + if (!empty($GLOBALS["_pluginLifecycleRespondido"])) return; + $salida = ""; + while (ob_get_level() > 0) { $salida .= (string) ob_get_clean(); } + echo json_encode(array( + "ok" => false, + "error" => "El gancho ha terminado con exit()/die() sin devolver resultado", + "messages" => $salida !== "" ? array(trim($salida)) : array(), + )); + }); + $event = is_scalar(@$datos["event"]) ? (string) $datos["event"] : ""; + $plugin = is_scalar(@$datos["plugin"]) ? (string) $datos["plugin"] : ""; + $origin = is_scalar(@$datos["origin"]) ? (string) $datos["origin"] : ""; $context = array( - "origin" => in_array(@$datos["origin"], array("forge", "legacy"), true) ? $datos["origin"] : "desconocido", + "origin" => in_array($origin, array("forge", "legacy"), true) ? $origin : "desconocido", "dropData" => filter_var(@$datos["dropData"], FILTER_VALIDATE_BOOLEAN), // Misma seƱal de entorno que header.php: el .acai solo existe en las copias locales. "mode" => file_exists(__DIR__ . "/../../../.acai") ? "local" : "production", ); - die(json_encode(runPluginLifecycle($plugin, $event, $context))); + $resultado = runPluginLifecycle($plugin, $event, $context); + $GLOBALS["_pluginLifecycleRespondido"] = true; + die(json_encode($resultado)); } function _removeFileRutaSegura($ruta, $esPlugin) { diff --git a/cms/lib/plugins/index.php b/cms/lib/plugins/index.php index 8e7a5a4..58e8a5b 100644 --- a/cms/lib/plugins/index.php +++ b/cms/lib/plugins/index.php @@ -369,6 +369,10 @@ function query($sql) // simplemente terminar. status.php devuelve semaforos: // [["clave" => "...", "estado" => "ok|warn|error", "texto" => "..."], ...]. // Lo que el gancho imprima se recoge como mensaje, nunca sale por la respuesta. +// El gancho ve $CURRENT_USER, $menu, $action, $TABLE_PREFIX y $SETTINGS como un +// checkpoint; cualquier otro global lo declara el. NUNCA exit()/die(): no es una +// excepcion, se salta el resultado y solo la red de seguridad de handler_ws.php +// evita una respuesta que no sea JSON. // --------------------------------------------------------------------------- function pluginLifecycleEvents() { return array("install", "uninstall", "enable", "disable", "status"); @@ -410,7 +414,9 @@ function installPluginArtifacts($path, &$messages) { } function _includePluginHook($hookFile, $plugin, $pluginDir, $event, $context, $dropData) { - // Ambito propio: el gancho no ve las variables de quien lo llama. + // Ambito propio: el gancho no ve las variables de quien lo llama, pero si + // los mismos globales del CMS que ve un checkpoint desde addPlugins(). + global $CURRENT_USER, $menu, $action, $TABLE_PREFIX, $SETTINGS; ob_start(); $ret = include $hookFile; $salida = trim((string) ob_get_clean());