Auditoría de Seguridad Laravel

AgendaFlow SaaS  ·  Laravel 11 + Sanctum + Livewire  ·  Pre-producción
4.5 /10
Puntuación de
Seguridad
Fecha: 18/06/2026 Revisado por: CULTIVA IA · Security Audit Versión Laravel: 11.x Stack: Sanctum · Livewire · MySQL · Redis · S3 Estado: ⚠ NO APTO PARA PRODUCCIÓN
3
Críticas
3
Altas
2
Medias
1
Bajas
🔴
Vulnerabilidades Críticas
Crítica
APP_DEBUG=true en producción — fuga total de configuración
.env  ·  config/app.php
CWE-215
Con APP_DEBUG=true cualquier excepción expone en el navegador: stack traces completos, valores de variables de entorno, credenciales de base de datos, APP_KEY y configuración interna. Un atacante puede obtener acceso total a la base de datos con un único error 500.
⚡ Exposición de credenciales DB ⚡ APP_KEY filtrada → sesiones falsificables ⏱ Fix: 2 min
✗ Vulnerable — .env actual
APP_DEBUG=true   # CRÍTICO
APP_ENV=local
APP_KEY=base64:abc...
✓ Corrección — .env producción
APP_DEBUG=false  # NUNCA true
APP_ENV=production
APP_KEY=base64:abc...
✓ Validación al arrancar — bootstrap/app.php
if (empty(config('app.key'))) {
    throw new RuntimeException(
        'APP_KEY no configurada. Ejecuta: php artisan key:generate'
    );
}
if (config('app.debug') && app()->environment('production')) {
    throw new RuntimeException('APP_DEBUG no puede ser true en producción.');
}
Crítica
Mass assignment desprotegido — escalada de privilegios
app/Models/User.php  ·  app/Http/Controllers/AppointmentController.php
CWE-915
$guarded = [] + create($request->all()) permite que un atacante inyecte campos como role=admin o is_verified=1 directamente en la petición HTTP. En un SaaS médico esto implica acceso a historiales de pacientes.
⚡ Escalada a admin con POST simple ⚡ Acceso a datos HIPAA ⏱ Fix: 15 min
✗ User.php vulnerable
class User extends Authenticatable
{
    protected $guarded = []; // PELIGROSO
    // role, is_admin, is_verified
    // todos escribibles!
}
✓ User.php corregido
final class User extends Authenticatable
{
    protected $fillable = [
        'name', 'email', 'phone',
    ]; // sin role, is_admin
}
✗ AppointmentController
public function store(Request $request)
{
    Appointment::create($request->all()); // ⚠
}
✓ AppointmentController corregido
public function store(StoreAppointmentRequest $r)
{
    Appointment::create($r->validated());
    // solo campos validados
}
Crítica
XSS Almacenado en notas de paciente
resources/views/patients/show.blade.php
CWE-79
El uso de {!! $patient->notes !!} renderiza HTML sin escapar. Un médico o paciente puede inyectar <script> en las notas y robar cookies/tokens de sesión de cualquier usuario que visite el perfil. En un entorno clínico esto compromete datos sensibles de salud.
⚡ Robo de sesión (cookies HTTPOnly bypass) ⚡ Datos sanitarios expuestos ⏱ Fix: 5 min
✗ Blade vulnerable
{{-- patients/show.blade.php --}}
<div class="notes">
    {!! $patient->notes !!} {{-- XSS --}}
</div>
✓ Blade corregido
{{-- patients/show.blade.php --}}
<div class="notes">
    {{ $patient->notes }} {{-- escapado --}}
</div>
{{-- o si necesitas HTML real: --}}
{!! app(Purifier::class)->clean($patient->notes) !!}
🟠
Severidad Alta
Alta
Upload de documentos sin validación de MIME — ejecución remota posible
app/Http/Controllers/Api/DocumentUploadController.php
CWE-434
El endpoint /api/upload-document no valida tipo MIME ni extensión. Un atacante puede subir un .php disfrazado de PDF y si el servidor Apache/Nginx sirve desde el directorio de subidas, se puede ejecutar código arbitrario.
⚡ RCE si la carpeta es pública ⏱ Fix: 20 min
✓ UploadDocumentRequest con validación completa
final class UploadDocumentRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'document' => ['required', 'file',
                'mimes:pdf,doc,docx',     // whitelist MIME
                'extensions:pdf,doc,docx', // whitelist extensión
                'max:10240',               // 10 MB máx
            ],
        ];
    }
}

// Almacenar FUERA del directorio público
$path = $request->file('document')->store('documents', 'local');
// NUNCA: store('documents', 'public')
Alta
Sin rate limiting en autenticación — fuerza bruta desprotegida
routes/api.php  ·  routes/web.php
CWE-307
Las rutas de login no tienen throttle. Un bot puede realizar miles de intentos por minuto contra cuentas de médicos y pacientes sin ningún bloqueo.
⚡ Brute force de contraseñas ⏱ Fix: 15 min
✓ RouteServiceProvider — rate limiters
RateLimiter::for('auth', fn (Request $req) =>
    Limit::perMinute(5)->by($req->ip())
);

RateLimiter::for('api', fn (Request $req) =>
    Limit::perMinute(60)->by($req->user()?->id ?: $req->ip())
);

// routes/web.php
Route::post('/login', [AuthController::class, 'login'])
    ->middleware('throttle:auth');
Alta
Sin cabeceras de seguridad HTTP — clickjacking y MIME sniffing
app/Http/Kernel.php
CWE-1021
Sin cabeceras como X-Frame-Options, Content-Security-Policy o X-Content-Type-Options la app es vulnerable a clickjacking (incrustar en iframe malicioso) y MIME sniffing (ejecutar scripts disfrazados de imágenes).
⚡ Clickjacking en panel médico ⏱ Fix: 20 min
✓ SecurityHeaders middleware
class SecurityHeaders
{
    public function handle($req, Closure $next): mixed
    {
        $res = $next($req);
        $res->headers->set('X-Frame-Options', 'DENY');
        $res->headers->set('X-Content-Type-Options', 'nosniff');
        $res->headers->set('X-XSS-Protection', '1; mode=block');
        $res->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
        $res->headers->set('Content-Security-Policy',
            "default-src 'self'; script-src 'self'; frame-ancestors 'none'"
        );
        return $res;
    }
}
🟡
Severidad Media
Media
Sesiones en driver file sin cifrado — exposición en servidor comprometido
config/session.php
CWE-311
Las sesiones guardadas en fichero son legibles en texto plano si el servidor se compromete. Para un SaaS médico con datos de pacientes, se recomienda el driver database o redis con cifrado activado.
⏱ Fix: 10 min
✗ session.php actual
'driver' => 'file',
'encrypt' => false,
'secure' => false,
✓ session.php corregido
'driver' => 'redis', // o database
'encrypt' => true,
'secure' => true, // HTTPS only
'http_only' => true,
'same_site' => 'lax',
Media
Sin log de intentos fallidos de autenticación — ausencia de auditoría
app/Listeners/FailedLoginListener.php (falta)
CWE-778
No hay registro de intentos fallidos de login ni cambios de rol. Imposible detectar intrusiones o cumplir normativa de auditoría (RGPD, esquemas médicos). Implementar un canal security en el logger.
⏱ Fix: 30 min
✓ SecurityLogger + listener
// app/Listeners/FailedLoginListener.php
class FailedLoginListener
{
    public function handle(Failed $event): void
    {
        Log::channel('security')->warning('failed_login', [
            'email' => $event->credentials['email'] ?? '?',
            'ip'    => request()->ip(),
            'ua'    => request()->userAgent(),
        ]);
    }
}

// EventServiceProvider
protected $listen = [
    Failed::class => [FailedLoginListener::class],
    PasswordReset::class => [SecurityAuditListener::class],
];
🟢
Severidad Baja
Baja
composer audit no ejecutado en CI — dependencias con CVEs conocidos
.github/workflows/ci.yml (falta)
CWE-1104
Sin composer audit en el pipeline CI, los CVEs en paquetes de terceros pasan desapercibidos. Añadir al workflow de GitHub Actions.
⏱ Fix: 5 min
✓ .github/workflows/security.yml
name: Security Audit
on: [push, pull_request]
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - run: composer audit --no-dev
      - run: php artisan about --only=environment
📋
Checklist Go-Live — AgendaFlow
APP_DEBUG=false en producción
Cambiar en .env y validar al arrancar
$fillable en todos los modelos
User, Appointment, Patient, Document
XSS: reemplazar {!! !!} por {{ }}
patients/show.blade.php y similares
Validación MIME en upload de documentos
UploadDocumentRequest + storage 'local'
Rate limiting en /login y /api
throttle:auth (5/min), throttle:api (60/min)
Middleware SecurityHeaders registrado
CSP, X-Frame-Options, X-Content-Type
Sesiones → driver redis/database + encrypt
Redis ya en stack, migración sencilla
Log de seguridad + listener Failed::class
Canal 'security' en config/logging.php
CSRF activo por defecto (VerifyCsrfToken)
Solo excluir webhooks con firma propia
Sanctum configurado para API móvil
Añadir expiración + abilities por ruta
composer audit en CI/CD pipeline
GitHub Actions — workflow security.yml
.env no commiteado en git
Verificado en .gitignore
HTTPS forzado en producción
URL::forceScheme('https') + ForceHttps middleware
Bcrypt rounds ≥ 12
config/hashing.php — aumentar de 10 a 12
🚫
Resultado: 8 fallos / 14 checks  ·  AgendaFlow NO está listo para producción. Estimado de remediación: 3–4 horas de desarrollo. Las 3 críticas deben corregirse antes de cualquier prueba con datos reales.
📅
Plan de Remediación — 2 semanas al go-live
Día 1 — URGENTE (hoy mismo)
Corregir las 3 vulnerabilidades críticas
APP_DEBUG=false $fillable en modelos XSS notes blade create($r->validated())
Días 2–3 — Alta prioridad
Corregir vulnerabilidades altas + CI
UploadDocumentRequest throttle:auth SecurityHeaders middleware ForceHttps middleware composer audit en CI
Días 4–7 — Media prioridad
Hardening y auditoría
Session → redis + encrypt SecurityLogger FailedLoginListener Bcrypt rounds 12
Días 8–14 — Pre-launch
Revisión final y pen-test ligero
Re-check checklist completo OWASP ZAP scan Revisar CORS allowed_origins php artisan about --env