diff --git a/app/Http/Controllers/WhatsappWebhookController.php b/app/Http/Controllers/WhatsappWebhookController.php index 3e5489e..6d0e952 100644 --- a/app/Http/Controllers/WhatsappWebhookController.php +++ b/app/Http/Controllers/WhatsappWebhookController.php @@ -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 // ────────────────────────────────────────────────────────── diff --git a/app/Http/Livewire/Whatsapp/ShowConfiguracionBot.php b/app/Http/Livewire/Whatsapp/ShowConfiguracionBot.php index 0a70601..1fbd19a 100644 --- a/app/Http/Livewire/Whatsapp/ShowConfiguracionBot.php +++ b/app/Http/Livewire/Whatsapp/ShowConfiguracionBot.php @@ -14,6 +14,7 @@ class ShowConfiguracionBot extends Component public string $phone_number_id = ''; public string $whatsapp_api_url = ''; public string $webhook_verify_token = ''; + public string $app_secret = ''; public string $bot_enabled = '1'; public string $welcome_message = ''; public string $default_no_match = ''; @@ -27,6 +28,7 @@ class ShowConfiguracionBot extends Component 'phone_number_id' => 'required|string', 'whatsapp_api_url' => 'required|url', 'webhook_verify_token' => 'required|string', + 'app_secret' => 'required|string', 'welcome_message' => 'required|string|max:1000', 'default_no_match' => 'required|string|max:500', 'advisor_message' => 'required|string|max:500', @@ -36,7 +38,7 @@ class ShowConfiguracionBot extends Component { $keys = [ 'whatsapp_token', 'phone_number_id', 'whatsapp_api_url', - 'webhook_verify_token', 'bot_enabled', 'welcome_message', + 'webhook_verify_token', 'app_secret', 'bot_enabled', 'welcome_message', 'default_no_match', 'advisor_message', 'business_hours_enabled', 'business_hours_start', 'business_hours_end', ]; @@ -52,7 +54,7 @@ class ShowConfiguracionBot extends Component $keys = [ 'whatsapp_token', 'phone_number_id', 'whatsapp_api_url', - 'webhook_verify_token', 'bot_enabled', 'welcome_message', + 'webhook_verify_token', 'app_secret', 'bot_enabled', 'welcome_message', 'default_no_match', 'advisor_message', 'business_hours_enabled', 'business_hours_start', 'business_hours_end', ]; diff --git a/database/migrations/2026_08_26_000001_add_app_secret_to_whatsapp_config.php b/database/migrations/2026_08_26_000001_add_app_secret_to_whatsapp_config.php new file mode 100644 index 0000000..8230d51 --- /dev/null +++ b/database/migrations/2026_08_26_000001_add_app_secret_to_whatsapp_config.php @@ -0,0 +1,25 @@ +updateOrInsert( + ['config_key' => 'app_secret'], + [ + 'config_value' => '', + 'description' => 'App Secret de Meta, firma los webhooks entrantes (X-Hub-Signature-256)', + 'created_at' => now(), + 'updated_at' => now(), + ] + ); + } + + public function down(): void + { + DB::table('whatsapp_system_config')->where('config_key', 'app_secret')->delete(); + } +}; diff --git a/resources/views/layouts/navigation.blade.php b/resources/views/layouts/navigation.blade.php index 65d00cb..8811e4d 100755 --- a/resources/views/layouts/navigation.blade.php +++ b/resources/views/layouts/navigation.blade.php @@ -252,7 +252,44 @@ {{-- Módulo WhatsApp (solo admins) --}} @if (in_array(auth()->user()->rol->nombre, ['super', 'administrador']))
{{ $message }}
@enderror + +Firma los webhooks entrantes. Sin este valor el webhook rechaza todo.
+ @error('app_secret'){{ $message }}
@enderror +