feat(whatsapp): validar firma de Meta en el webhook y restaurar el menú del módulo

- POST /webhooks exige X-Hub-Signature-256 (HMAC-SHA256 del cuerpo crudo);
  falla cerrado si app_secret no está configurado
- nueva clave app_secret en whatsapp_system_config, editable desde Configuración Bot
- menú lateral de WhatsApp visible de nuevo (Correo/IMAP sigue en Chat/Bot)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-26 23:02:39 -05:00
co-authored by Claude Opus 5
parent 5e0c0367d9
commit e3ad0a423a
6 changed files with 133 additions and 3 deletions
@@ -45,6 +45,12 @@ class WhatsappWebhookController extends Controller
public function handle(Request $request)
{
// Solo Meta puede publicar aquí: firma HMAC-SHA256 del cuerpo crudo con el App Secret.
// Falla cerrado — sin secreto configurado no se procesa nada.
if (! $this->signatureValid($request)) {
return response()->json(['status' => 'forbidden'], 403);
}
$payload = $request->all();
// Loguear el payload entrante antes de procesar
@@ -66,6 +72,36 @@ class WhatsappWebhookController extends Controller
return response()->json(['status' => 'ok']);
}
// ──────────────────────────────────────────────────────────
// Verificación de la firma X-Hub-Signature-256 de Meta
// ──────────────────────────────────────────────────────────
private function signatureValid(Request $request): bool
{
$secret = WhatsappSystemConfig::get('app_secret');
if ($secret === '') {
Log::error('[WhatsApp Webhook] app_secret sin configurar; payload rechazado.');
return false;
}
$header = $request->header('X-Hub-Signature-256', '');
if (! str_starts_with($header, 'sha256=')) {
Log::warning('[WhatsApp Webhook] Falta cabecera X-Hub-Signature-256.');
return false;
}
$esperada = hash_hmac('sha256', $request->getContent(), $secret);
if (! hash_equals($esperada, substr($header, 7))) {
Log::warning('[WhatsApp Webhook] Firma inválida; payload rechazado.');
return false;
}
return true;
}
// ──────────────────────────────────────────────────────────
// Extracción y enrutamiento del payload
// ──────────────────────────────────────────────────────────