Un escáner forense para detectar cambios sospechosos en Laravel
Laravel Scalpel revisa el filesystem de una aplicación para encontrar señales de intrusión y cambios no autorizados.
Cuando el problema está en el disco
Una aplicación Laravel puede seguir respondiendo con normalidad aunque alguien haya dejado un archivo ejecutable donde no debería estar. Un webshell dentro de public, una regla añadida a .htaccess o un .env modificado no siempre deja una pista clara en los logs de la aplicación. En un servidor autoalojado, además, muchas veces no hay un equipo de seguridad ni una plataforma externa vigilando cada cambio. Ahí es donde un escaneo del filesystem tiene sentido como comprobación adicional.
Laravel Scalpel es un paquete que añade comandos Artisan para buscar evidencia de intrusión en el proyecto. No intenta analizar el tráfico HTTP ni actuar como firewall. Su foco está en los archivos presentes, su contenido y las diferencias respecto a un estado conocido. Me parece especialmente práctico para instancias Laravel que viven en un homelab, un VPS o un servidor con despliegues que uno mismo mantiene.
Qué revisa el paquete
El comando principal, scalpel:scan, agrupa varios escáneres con objetivos distintos. Puede encontrar archivos PHP en rutas configuradas como zonas donde no deberían existir, incluyendo extensiones como .phtml o dobles extensiones del tipo archivo.php.jpg. También busca patrones habituales de ofuscación en código PHP y revisa reglas peligrosas en archivos .htaccess. El paquete incluye además un análisis de .user.ini y comprobaciones de integridad básicas para .env.
La utilidad de estas revisiones no está en asumir que toda coincidencia es un ataque. Un resultado debe servir para abrir una investigación: comprobar quién desplegó el archivo, contrastarlo con Git y revisar los permisos del servidor. Precisamente por eso conviene definir las excepciones reales de cada proyecto en lugar de ignorar alertas de forma indiscriminada. Las rutas permitidas y las zonas sin PHP se configuran en config/scalpel.php.
composer require hryagstn/laravel-scalpel
php artisan vendor:publish --tag=scalpel-config
php artisan scalpel:scanUn baseline para saber qué ha cambiado
La parte más útil para un servidor persistente es el baseline. Con php artisan scalpel:baseline se crea una instantánea del filesystem, y php artisan scalpel:diff compara el estado actual con ella. El paquete calcula hashes SHA-256 para detectar archivos añadidos, eliminados o modificados. El baseline se debe generar después de un despliegue que consideremos limpio, no antes de instalar dependencias o regenerar cachés.
El comportamiento por defecto excluye rutas que cambian con frecuencia, como partes de storage, pero no conviene aceptar esa lista sin revisarla. Cada aplicación puede tener directorios de subidas, exportaciones o cachés propios que generarían ruido en una comparación. Por otro lado, excluir una ruta también reduce la capacidad de detectar modificaciones dentro de ella. La regla práctica es sencilla: excluir solo lo que realmente sea volátil y no tenga valor como evidencia.
// config/scalpel.php
'non_php_zones' => [
'public',
'storage',
],
'baseline_excluded_paths' => [
'storage/logs',
'storage/framework/cache',
'storage/framework/sessions',
],Integrarlo después del despliegue
Yo usaría el baseline como una tarea posterior al despliegue, una vez terminadas las operaciones normales de Laravel. Si el proceso ejecuta php artisan optimize, hay que hacerlo antes de actualizar la instantánea, porque esos archivos pueden cambiar legítimamente. Después, un cron externo puede ejecutar scalpel:diff de forma periódica sobre el servidor. Así se detectan cambios que ocurran entre despliegues, que son los que más interesa investigar.
También se puede usar en CI, aunque conviene separar dos casos. En una pull request, scalpel:scan sirve para revisar el árbol que se va a desplegar y producir resultados consumibles por herramientas. En cambio, scalpel:diff necesita acceder a un baseline válido del entorno que queremos vigilar. Guardarlo en el repositorio puede ser apropiado en algunos flujos, pero para un servidor autoalojado suele tener más sentido conservarlo fuera del directorio público y actualizarlo durante el despliegue.
- name: Ejecutar Laravel Scalpel
run: php artisan scalpel:scan --format=sarif > scalpel.sarif
- name: Subir el informe SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: scalpel.sarifEl formato sarif permite enviar hallazgos al análisis de código de GitHub. Si prefiero que aparezcan anotaciones directamente en la ejecución de Actions, el paquete ofrece --format=github. Para hacer que una alerta rompa el proceso, está la opción --fail-on, que acepta los niveles CRITICAL, HIGH, MEDIUM o LOW. El valor que elija debe depender de la tolerancia al ruido del proyecto, no de una política copiada sin contexto.
La letra pequeña que importa
Laravel Scalpel es un detector, no una barrera de contención. Se ejecuta dentro de Laravel y normalmente comparte usuario, permisos y filesystem con la propia aplicación. Si un atacante ya puede escribir código arbitrario en el servidor, podría alterar el paquete, su configuración o los informes antes de que lleguen a otro sistema. Ningún comando Artisan resuelve por sí solo ese problema de confianza.
Tampoco sustituye a mantener PHP, Laravel y las dependencias actualizados, ni a limitar permisos de escritura. En un despliegue con Docker, separar el código en un filesystem de solo lectura y dejar escribibles únicamente los directorios necesarios reduce mucho la superficie de ataque. Ejecutar el escaneo desde cron o desde un sistema externo, y enviar los resultados fuera del servidor, mejora el valor de las alertas. El paquete encaja como alarma temprana, no como sustituto de un firewall, copias de seguridad o monitorización a nivel de sistema.
Hay que contar también con falsos positivos. Código heredado, librerías internas, tareas de generación de archivos o reglas legítimas de Apache pueden activar una detección. Antes de suprimir una alerta, revisaría el archivo, su procedencia y el cambio en control de versiones. Si un resultado aparece sin una explicación operativa clara, tratarlo como incidente potencial suele ser la decisión correcta.
Fuente: Laravel News
Documentación oficial: Laravel Scalpel en GitHub