Laravel 13: checklist de seguridad para producción en 2026
Tu app Laravel funciona: el login entra, las rutas responden... pero la seguridad no es un interruptor que enciendes, es una lista de capas — .env, headers, XSS, CSRF, dependencias — que se revisan antes y después del deploy. Esta es la checklist de seguridad para producción en Laravel 13 para 2026.
Por qué la seguridad en Laravel se revisa por capas
La seguridad de una aplicación web no se resuelve con una sola medida: se construye por capas, de forma que si una falla, la siguiente todavía te protege. En Laravel, cada capa tiene nombre propio — configuración, transporte, cabeceras, validación, autenticación, dependencias — y esta guía las recorre en el mismo orden en que se revisa un deploy.
Seguro por defecto, inseguro por configuración
Laravel es seguro por defecto en muchas de sus piezas: Blade escapa la salida, Eloquent usa consultas parametrizadas y los formularios llevan token CSRF. El problema aparece cuando la configuración rompe esa seguridad: un APP_DEBUG activo en producción, un modelo sin $fillable o un controlador que confía en el input crudo. La vulnerabilidad casi nunca está en el framework, está en la capa mal configurada.
OWASP Top 10 y las protecciones que Laravel ya trae
El OWASP Top 10 describe las vulnerabilidades web más explotadas, y la serie de cheat sheets de OWASP incluye una específica para Laravel. La buena noticia: inyección SQL, XSS y CSRF — tres clásicos del Top 10 — tienen mitigación nativa en el framework. El trabajo real es activarlas bien y combinarlas con validación, control de acceso y auditoría de dependencias.
Entorno y configuración antes de nada
APP_ENV=production y APP_DEBUG=false
Lo primero que se revisa en un deploy es el entorno. En el .env de producción, APP_ENV debe valer production y APP_DEBUG debe estar en false. Con APP_DEBUG activo, cualquier error muestra la página detallada de Laravel con rutas internas, variables de entorno y trazas que delatan la arquitectura de la app; en producción eso es información que no debería salir nunca.
APP_KEY generada y secretos fuera del repositorio
La APP_KEY cifra las sesiones y los datos sensibles: si no existe o se genera con un valor predecible, el cifrado no sirve. Se genera con php artisan key:generate y debe ser distinta en cada entorno. Además, los secretos — claves de API, credenciales de base de datos, tokens — viven en el .env, que nunca se sube al repositorio: el .env debe estar en .gitignore y en producción se configura fuera del control de versiones.
Permisos del .env y del storage
El .env solo lo necesita el usuario del servidor que ejecuta PHP. Revisa que no sea legible por el usuario del servidor web público y que el storage tenga los permisos mínimos para que la app escriba logs y archivos subidos. Un .env servido como archivo estático es una fuga de secretos en una sola petición.
HTTPS y cookies seguras
Forzar HTTPS y SESSION_SECURE_COOKIE
Toda la app debe servirse por HTTPS, y las cookies de sesión solo deben viajar por ese canal. En el .env de producción: SESSION_SECURE_COOKIE=true. Así el navegador no envía la cookie de sesión por HTTP, y un secuestro de sesión en una red intermedia se vuelve mucho más difícil. En el servidor, redirige HTTP a HTTPS y configura TrustProxies si la app está detrás de un proxy o balanceador.
HSTS: que el navegador solo vuelva por HTTPS
Además de redirigir, conviene declarar la política Strict-Transport-Security (HSTS) con una cabecera: el navegador recuerda que ese dominio solo se habla por HTTPS y ni siquiera intenta la versión insegura. Se añade con un middleware propio o con librerías de cabeceras seguras, apuntando al dominio concreto y con un max-age razonable.
Cabeceras de seguridad HTTP
X-Content-Type-Options y X-Frame-Options (clickjacking)
Dos cabeceras pequeñas que cierran agujeros clásicos. X-Content-Type-Options: nosniff evita que el navegador adivine el tipo de un recurso (MIME sniffing) y ejecute scripts disfrazados de imagen o CSS. X-Frame-Options (o frame-ancestors en la CSP) impide que otra web incruste tu app en un iframe y monte un ataque de clickjacking.
Content-Security-Policy para mitigar XSS
La Content-Security-Policy (CSP) limita los orígenes desde los que el navegador puede cargar scripts, estilos e imágenes. Es una segunda línea contra XSS: aunque un atacante logre inyectar una etiqueta script, la CSP bloquea su ejecución si el origen no está permitido. Una política estricta se configura de menos a más: empieza monitorizando y endurece poco a poco para no romper tu propio front.
Un middleware de headers propio en cinco líneas
En Laravel puedes agrupar estas cabeceras en un middleware propio de pocas líneas y aplicarlo a las rutas web:
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$response->headers->set('Content-Security-Policy', "default-src 'self'");
return $response;
}Regístralo en el grupo web y comprueba con las herramientas de desarrollo del navegador que las cabeceras llegan en cada respuesta.
El código que recibe datos del usuario
XSS: Blade {{ }} escapa, {!! !!} solo con HTML confiable
Blade escapa la salida por defecto: {{ $variable }} convierte los caracteres peligrosos en entidades y un intento de script se muestra como texto inofensivo. El riesgo llega con {!! $variable !!}, que imprime HTML crudo. Úsalo solo cuando sepas que el contenido es confiable o está sanitizado antes; para HTML generado por el usuario, pasa por un sanitizador antes de imprimirlo.
Validación: Form Requests con reglas explícitas
Cada acción que recibe input del usuario debe validar ese input con reglas explícitas. La forma limpia en Laravel es una clase Form Request con su método rules: define qué espera cada campo, con tipo, formato, tamaño y límites. Nunca confíes en $request->all() para asignar a un modelo sin pasar por validación y por el control de campos asignables.
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255', 'unique:users,email'],
'avatar' => ['required', 'image', 'mimes:jpg,png,webp', 'max:2048'],
];
}Mass assignment: $fillable y $guarded en cada modelo
El mass assignment ocurre cuando asignas varios campos de golpe a un modelo — por ejemplo desde el input completo del usuario — y un campo que no debía cambiarse (un rol, un saldo, un flag de admin) se cuela en la asignación. Se controla declarando en cada modelo qué campos son asignables ($fillable) o, al revés, cuáles están blindados ($guarded). Si un modelo no tiene ninguna de las dos, revisa por qué: esa es la excepción que hay que justificar.
SQL injection: query builder con parámetros y cuidado con whereRaw
Eloquent y el query builder protegen de la inyección SQL porque bindean los valores como parámetros en lugar de concatenarlos. El peligro aparece cuando concatenas input del usuario dentro de un whereRaw o un DB::raw. Si necesitas SQL crudo, pasa los valores como bindings, nunca interpolados en la cadena.
// Seguro: el query builder escapa el valor como parámetro
$users = User::where('email', $request->input('email'))->get();
// Si usas whereRaw, pasa bindings, no concatenes
$users = User::whereRaw('email = ?', [$request->input('email')])->get();Autenticación y sesiones
CSRF: el token automático y cuándo no desactivarlo
Laravel protege los formularios con un token CSRF automático: cada petición de escritura del navegador debe llevarlo o el framework la rechaza. La tentación de desactivarlo — en el middleware VerifyCsrfToken o con @csrf olvidado — aparece con integraciones raras, pero casi nunca es la solución correcta. Mantén el token en todos los formularios y usa rutas API con su propia autenticación cuando el cliente no sea un formulario del navegador.
Rate limiting en el login
Un formulario de login sin límite es una puerta abierta a probar contraseñas en bucle. Laravel trae rate limiting de serie: aplica un throttle a las rutas de login para limitar los intentos por IP o por usuario en una ventana de tiempo. El blog tiene un tutorial completo de rate limiting con Laravel 13 si necesitas el detalle de configuración.
Segundo factor: 2FA/TOTP y passkeys
Si la app guarda datos de clientes o de negocio, la contraseña sola no basta. El segundo factor — un TOTP tipo Google Authenticator o passkeys — bloquea el acceso aunque una contraseña se filtre. No hace falta construirlo a mano: hay paquetes consolidados y en el blog tienes guías de 2FA/TOTP y passkeys en Laravel 13 para integrarlos paso a paso.
APIs: Sanctum y CORS
Tokens con Sanctum y alcances mínimos
Para una API propia, Sanctum emite tokens con alcances: cada token solo debería poder hacer lo que necesita quien lo usa. Define scopes mínimos por tipo de cliente y revísalos como revisarías permisos de usuarios. Un token con acceso total en un cliente que solo lee catálogo es una superficie de ataque innecesaria.
CORS: solo los orígenes que de verdad consumen tu API
La configuración CORS decide qué orígenes del navegador pueden llamar a tu API. Déjala en el valor por defecto amplio solo durante el desarrollo; en producción, lista únicamente los dominios que de verdad consumen la API. Un CORS abierto convierte tu API en un recurso que cualquier web puede llamar desde el navegador de un usuario autenticado.
Subidas de archivos seguras
Las subidas de archivos se validan en dos frentes. Primero, el contenido: tipo MIME, extensión, tamaño máximo y, si el archivo es una imagen, comprobar que realmente lo es — un PNG falso puede esconder un script. Segundo, el almacenamiento: guarda los archivos fuera de la raíz pública y sírvelos por una ruta controlada o por disco privado, para que nunca se ejecuten como código en el dominio. El blog tiene un post de file uploads y storage en Laravel 13 con el detalle.
Dependencias: composer audit en local y en CI
Qué reporta composer audit y cómo leerlo
Las vulnerabilidades también llegan por las dependencias: un paquete con una falla conocida puede abrir la app aunque tu código esté impecable. Composer incluye el comando composer audit, que consulta los security advisories de Packagist y lista los paquetes afectados, la versión vulnerable y la versión corregida. Ejecútalo en local antes de cada release:
composer audit
Cuando reporte algo, la lectura es directa: actualiza el paquete a la versión corregida y repite hasta que la salida esté limpia. Si una dependencia no tiene corrección disponible, evalúa reemplazarla o aislar su uso.
Automatizar con Dependabot y el pipeline de CI
Hacer composer audit a mano se olvida. Automatízalo: añade el comando al pipeline de CI para que falle el deploy si hay vulnerabilidades conocidas, y activa Dependabot para recibir pull requests cuando una dependencia tenga una corrección de seguridad. Así la auditoría de dependencias se convierte en parte del flujo y no en un pendiente que nunca llega.
Checklist final: 10 puntos para producción
Para cerrar, los diez puntos verificables que puedes repasar en una tarde antes de cada deploy:
- APP_ENV=production y APP_DEBUG=false en el .env del servidor.
- APP_KEY generada y distinta por entorno; secretos fuera del repositorio.
- Toda la app por HTTPS, con SESSION_SECURE_COOKIE=true y HSTS declarado.
- Cabeceras de seguridad presentes: X-Content-Type-Options, X-Frame-Options y CSP.
- Salidas en Blade con {{ }}; {!! !!} solo con HTML confiable y sanitizado.
- Validación con Form Requests en cada acción que recibe input.
- $fillable o $guarded definido en cada modelo Eloquent.
- CSRF activo en formularios, rate limiting en login y 2FA donde haya datos sensibles.
- API con tokens Sanctum de alcance mínimo y CORS restringido a tus orígenes.
- composer audit limpio, ejecutado en local y automatizado en CI.
Conclusion
La seguridad en Laravel no es un estado que se alcanza un día: es una revisión periódica de capas que el framework ya te da casi resueltas — Blade escapa, Eloquent parametriza, CSRF protege — y que solo se rompen con una mala configuración. Corre la checklist antes de cada deploy, automatiza lo que puedas con composer audit y CI, y trata la seguridad como parte del desarrollo, no como un trámite final. Si esta guía te sirvió, en el blog tienes tutoriales en profundidad de rate limiting, 2FA, Sanctum y despliegue en VPS con Laravel 13.