From a9ecad4ee1cbbf29667d5a63a2e39709030c8953 Mon Sep 17 00:00:00 2001 From: Jordan Diaz Date: Tue, 22 Sep 2026 16:27:18 +0000 Subject: [PATCH] fix(core): los ganchos de ciclo de vida de plugins funcionan en las webs locales MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _pluginLifecycle validaba el token SOLO contra el WS central, que unicamente conoce webs de produccion: en una copia local de Forge (standalone o de test) el token nunca le valida y instalar, desinstalar, activar o desactivar un plugin moria con "Token no valido", un mensaje que no tenia nada que ver con el token. Segunda via para esas copias: si el central rechaza y la web tiene .acai con mode != production, se compara el par token+tokenHash recibido con el que la propia web guarda en su .acai (hash_equals). No es un bypass — sin .acai, en produccion o sin coincidencia exacta no autoriza nada —, y el secreto es el mismo que Forge ya usa para hablar con la web. Probado en superadmin/claude-es-mi-amigo: con el token del .acai el gancho corre (ok:true); con un token falso o vacio, 403 como siempre. Co-Authored-By: Claude Opus 5 (1M context) --- cms/lib/handlers/handler_ws.php | 34 ++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/cms/lib/handlers/handler_ws.php b/cms/lib/handlers/handler_ws.php index faaa35f..ada5108 100755 --- a/cms/lib/handlers/handler_ws.php +++ b/cms/lib/handlers/handler_ws.php @@ -399,13 +399,45 @@ function _renameFile() { } +/** + * Segunda via de autenticacion del ciclo de vida, para las copias LOCALES de + * Forge (standalone o de test). El WS central solo conoce webs de produccion: + * el token de una web sin dominio real nunca le valida, asi que los ganchos + * (install/uninstall/enable/disable) quedaban inservibles ahi con un + * "Token no valido" que no tenia nada que ver con el token. + * + * No es un bypass: se compara el par token+tokenHash recibido con el que la + * propia web guarda en su .acai, que es exactamente el que Forge envia. Sin + * .acai (produccion), con mode=production, o sin coincidencia exacta, no + * autoriza nada. + */ +function _lifecycleTokenLocalValido($datosAuth) { + $acaiPath = __DIR__ . "/../../../.acai"; + if (!is_file($acaiPath)) { + return false; + } + $acai = @json_decode(@file_get_contents($acaiPath), true); + if (!is_array($acai) || @$acai["mode"] === "production") { + return false; + } + $token = (string) @$datosAuth["token"]; + $hash = (string) @$datosAuth["tokenHash"]; + $tokenLocal = (string) @$acai["token"]; + $hashLocal = (string) @$acai["tokenHash"]; + if ($token === "" || $hash === "" || $tokenLocal === "" || $hashLocal === "") { + return false; + } + return hash_equals($tokenLocal, $token) && hash_equals($hashLocal, $hash); +} + function _pluginLifecycle() { // Fase 0 (2026-09-12): ejecuta los ganchos de ciclo de vida de un plugin // (install/uninstall/enable/disable/status) DENTRO de la web. Unico punto // de entrada para Forge y para el CMS legacy. Misma autenticacion que coreUpdate. $datosAuth = json_decode(file_get_contents('php://input'), true); CocoWS::setToken(@$datosAuth["token"]); - if (!CocoWS::validateUploadToken(@$datosAuth["tokenHash"])) { + if (!CocoWS::validateUploadToken(@$datosAuth["tokenHash"]) + && !_lifecycleTokenLocalValido($datosAuth)) { die(json_encode(['error' => ['message' => 'Token no válido', 'code' => 403]])); } // Solo body JSON (como _saveFile): el token no debe viajar en la query string.