Práticas Recomendadas de Segurança no Laravel
Diretrizes de segurança abrangentes para aplicações Laravel para proteger contra vulnerabilidades comuns.
Quando Ativar
- Configurando autenticação e autorização no Laravel (Sanctum, Passport, Jetstream, Breeze)
- Implementando funções de usuário, permissões e políticas
- Configurando configurações de segurança de produção e variáveis de ambiente
- Revisando aplicações Laravel em busca de vulnerabilidades de segurança
- Implantando aplicações Laravel em produção
- Escrevendo queries e migrações Eloquent seguras
Configuração de Produção
Configurações Essenciais de Produção
// config/app.php
'env' => env('APP_ENV', 'production'),
'debug' => (bool) env('APP_DEBUG', false), // CRÍTICO: Nunca true em produção
'key' => env('APP_KEY'), // Deve ser definido: php artisan key:generate
// config/session.php
'secure' => env('SESSION_SECURE_COOKIE', true),
'http_only' => true,
'same_site' => 'lax',
// Verificar se APP_KEY está definido na inicialização
// bootstrap/app.php ou um service provider
if (empty(config('app.key'))) {
throw new RuntimeException('APP_KEY não está definido. Execute: php artisan key:generate');
}
Segurança do Arquivo de Ambiente
# NUNCA commitar .env no controle de versão
# .gitignore já inclui .env por padrão
# Use .env.example com placeholders
DB_PASSWORD=
APP_KEY=
SANCTUM_TOKEN_PREFIX=
# Validar variáveis obrigatórias na inicialização
// Em AppServiceProvider::boot()
$requiredKeys = ['app.key', 'database.connections.mysql.database', 'database.connections.mysql.username'];
foreach ($requiredKeys as $key) {
if (empty(config($key))) {
throw new RuntimeException("Chave de configuração obrigatória ausente: {$key}");
}
}
Forçar HTTPS
// AppServiceProvider::boot() ou middleware
if (app()->environment('production')) {
URL::forceScheme('https');
request()->server->set('HTTPS', 'on');
}
// config/app.php para proxies confiáveis (load balancers)
// Use faixas de IP específicas — * confia em todos, permitindo falsificação X-Forwarded-*
// AWS: '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'
'trusted_proxies' => ['10.0.0.0/8', '172.16.0.0/12'],
// Forçar HTTPS em produção via middleware
// app/Http/Middleware/ForceHttps.php
public function handle($request, Closure $next)
{
if (!$request->secure() && app()->environment('production')) {
return redirect()->secure($request->getRequestUri());
}
return $next($request);
}
Autenticação
Sanctum (Autenticação por Token de API)
// config/sanctum.php
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
env('APP_URL') ? ',' . parse_url(env('APP_URL'), PHP_URL_HOST) : ''
)));
'expiration' => 60 * 24, // Expiração do token em minutos (null = nunca)
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
// Emitindo tokens com habilidades
$token = $user->createToken('api-token', ['read', 'write'])->plainTextToken;
// Validar habilidades nas rotas
Route::middleware('auth:sanctum')->group(function () {
Route::get('/orders', function () {
// Usuário deve ter habilidade 'read'
abort_unless(Auth::user()->tokenCan('read'), 403);
// ...
})->middleware('abilities:read');
Route::post('/orders', function () {
// Usuário deve ter habilidade 'write'
abort_unless(Auth::user()->tokenCan('write'), 403);
// ...
})->middleware('abilities:write');
});
Segurança de Senhas
// config/hashing.php
// O padrão é bcrypt. Argon2id é mais forte.
'bcrypt' => [
'rounds' => env('BCRYPT_ROUNDS', 12), // Aumente para hash mais forte
],
'argon' => [
'memory' => 65536,
'threads' => 4,
'time' => 4,
],
// Validação de senha em RegisterRequest
public function rules(): array
{
return [
'password' => [
'required',
'confirmed',
Password::min(12)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised(), // Verifica haveibeenpwned
],
];
}
// Limitar tentativas de login
// App\Http\Controllers\Auth\AuthenticatedSessionController
protected function authenticated(Request $request, $user)
{
if ($user->wasRecentlyLockedOut()) {
// Notificar usuário sobre login suspeito
$user->notify(new SuspiciousLoginNotification($request->ip()));
}
}
Gerenciamento de Sessão
// config/session.php
'driver' => env('SESSION_DRIVER', 'database'), // database/redis > file
'lifetime' => env('SESSION_LIFETIME', 120),
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
'encrypt' => env('SESSION_ENCRYPT', false),
// Regenerar sessão no login
// App\Http\Controllers\Auth\AuthenticatedSessionController
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate(); // CRÍTICO: previne fixação de sessão
return redirect()->intended(RouteServiceProvider::HOME);
}
// Invalidar sessão no logout
public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
Autorização
Gates
// App\Providers\AuthServiceProvider
use App\Models\Post;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
public function boot(): void
{
Gate::define('update-post', function (User $user, Post $post): bool {
return $user->id === $post->user_id;
});
Gate::define('publish-post', function (User $user): bool {
return $user->role === 'editor' || $user->role === 'admin';
});
// Usando before() para sobrescrita de super-admin
Gate::before(function (User $user, string $ability): ?bool {
if ($user->role === 'super-admin') {
return true; // Concede todas as habilidades
}
return null; // Continua para verificações normais
});
}
// Uso em controllers
public function update(Request $request, Post $post): RedirectResponse
{
Gate::authorize('update-post', $post);
// Ou: $this->authorize('update-post', $post);
// Ou: abort_unless(Auth::user()->can('update-post', $post), 403);
// ...
}
Policies
// App\Policies\PostPolicy
class PostPolicy
{
use HandlesAuthorization;
public function viewAny(?User $user): bool
{
return true; // Listagem pública
}
public function view(?User $user, Post $post): bool
{
return $post->is_published || ($user && $user->id === $post->user_id);
}
public function create(User $user): bool
{
return $user->hasVerifiedEmail(); // Deve verificar email primeiro
}
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
public function delete(User $user, Post $post): bool
{
return $user->id === $post->user_id && $post->created_at->diffInDays(now()) <= 30;
}
public function restore(User $user, Post $post): bool
{
return $user->role === 'admin';
}
public function forceDelete(User $user, Post $post): bool
{
return $user->role === 'super-admin';
}
}
// Registrar em AuthServiceProvider
protected $policies = [
Post::class => PostPolicy::class,
];
// Uso em controller
public function show(Post $post): View
{
$this->authorize('view', $post);
return view('posts.show', compact('post'));
}
// Uso no Blade
@can('update', $post)
<a href="{{ route('posts.edit', $post) }}">Editar</a>
@endcan
@cannot('update', $post)
<span>Você não pode editar este post</span>
@endcannot
Autorização via Middleware
// Usando middleware nas rotas
Route::put('/posts/{post}', [PostController::class, 'update'])
->middleware('can:update,post');
Route::get('/posts/create', [PostController::class, 'create'])
->middleware('can:create,App\Models\Post');
// Middleware de autorização personalizado
// app/Http/Middleware/CheckRole.php
class CheckRole
{
public function handle(Request $request, Closure $next, string $role): mixed
{
if (!$request->user() || $request->user()->role !== $role) {
abort(403, 'Não autorizado. Esta área requer a função: ' . $role);
}
return $next($request);
}
}
// Registrar em Kernel
protected $routeMiddleware = [
'role' => \App\Http\Middleware\CheckRole::class,
];
// Uso em rota
Route::middleware(['auth', 'role:admin'])->group(function () {
Route::get('/admin', [AdminController::class, 'index']);
});
Segurança no Eloquent
Proteção contra Atribuição em Massa
// RUIM: $guarded = [] permite que TODAS as colunas sejam atribuídas em massa
// NUNCA use $guarded = [] em produção
// BOM: Lista branca de atributos fillable
final class User extends Authenticatable
{
protected $fillable = [
'name',
'email',
'phone',
'avatar',
];
// NUNCA adicione 'role', 'is_admin', 'is_verified' aqui
}
// BOM: Controle explícito de quais campos podem ser preenchidos nas requisições
public function store(StoreUserRequest $request): RedirectResponse
{
$user = User::create($request->safe()->only([
'name', 'email', 'phone', 'avatar'
]));
// $request->safe() usa apenas dados validados
// $request->only() NÃO é seguro por si só sem regras de validação
}
// RUIM: Criando usuário com dados da requisição diretamente
User::create($request->all()); // VULNERÁVEL a atribuição em massa!
// MELHOR: Use DTOs para criação
$user = User::create($request->validated()); // Apenas campos validados
Prevenção contra Injeção SQL
// BOM: Eloquent parametriza queries automaticamente
User::where('email', $userInput)->first();
User::whereRaw('email = ?', [$userInput])->first();
// BOM: Query Builder também parametriza
DB::table('users')->where('email', $userInput)->first();
DB::select('SELECT * FROM users WHERE email = ?', [$userInput]);
// RUIM: Interpolação direta de string
DB::select("SELECT * FROM users WHERE email = '{$userInput}'"); // VULNERÁVEL!
User::whereRaw("email = '{$userInput}'")->first(); // VULNERÁVEL!
// RUIM: whereRaw/orderByRaw com entrada sem escape
User::orderByRaw($userInput); // VULNERÁVEL!
User::groupByRaw($userInput); // VULNERÁVEL!
// RUIM: DB::statement com concatenação
DB::statement("INSERT INTO users (email) VALUES ('{$userInput}')"); // VULNERÁVEL!
Casting de Atributos
final class User extends Authenticatable
{
protected $casts = [
'email_verified_at' => 'datetime',
'is_admin' => 'boolean', // Cast para booleano previne injeção de string
'settings' => 'array', // Automaticamente json_encode/json_decode
'metadata' => 'encrypted:array', // Laravel 11+ casting criptografado
'password' => 'hashed', // Laravel 10+ auto-hash ao definir
];
}
Segurança do Modelo
final class User extends Authenticatable
{
// Ocultar atributos sensíveis de respostas JSON/API
protected $hidden = [
'password',
'remember_token',
'two_factor_secret',
'two_factor_recovery_codes',
];
// Adicionar apenas atributos computados seguros
protected $appends = ['full_name']; // seguro
// NUNCA adicionar dados computados sensíveis
}
final class Post extends Model
{
// Escopo global para filtrar registros com soft delete
use SoftDeletes;
// Prevenir N+1 restringindo lazy loading (modo estrito opcional)
// AppServiceProvider::boot()
// Model::preventLazyLoading(!app()->isProduction());
}
Proteção CSRF
Proteção Padrão
// CSRF do Laravel está habilitado por padrão via middleware VerifyCsrfToken
// app/Http/Kernel.php (protected $middlewareGroups['web'])
// Todos os formulários POST/PUT/PATCH/DELETE devem incluir @csrf
<form method="POST" action="/posts">
@csrf
<input type="text" name="title">
<button type="submit">Criar</button>
</form>
Excluindo Rotas (Com Cuidado)
// app/Http/Middleware/VerifyCsrfToken.php
class VerifyCsrfToken extends Middleware
{
// Apenas exclua rotas que têm proteção CSRF externa (webhooks, etc.)
protected $except = [
'stripe/*', // Webhooks do Stripe usam sua própria verificação de assinatura
// Evite 'api/*' genérico — rotas stateful do Sanctum precisam de CSRF.
// Exclua apenas rotas específicas de webhook/endpoint stateless.
];
}
CSRF com JavaScript
<meta name="csrf-token" content="{{ csrf_token() }}">
<script>
// Exemplo com Axios (Laravel acompanha Axios)
axios.defaults.headers.common['X-CSRF-TOKEN'] = document.querySelector(
'meta[name="csrf-token"]'
).getAttribute('content');
// Exemplo com Fetch
fetch('/posts', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
</script>
Prevenção de XSS
Segurança no Blade Templating
{{-- SEGURO: Escapado automaticamente pelo Blade --}}
{{ $userInput }}
{{-- PERIGOSO: Saída bruta — NUNCA use com entrada do usuário --}}
{!! $userInput !!}
{{-- SEGURO: Use {!! !!} apenas com conteúdo confiável que você controla --}}
{!! $trustedHtmlFromYourServer !!}
{{-- BOM: Use diretivas de escape específicas --}}
@js($data) {{-- Codifica JSON para JavaScript --}}
@json($data) {{-- Codifica JSON em templates --}}
{{-- RUIM: Entrada direta do usuário em HTML bruto --}}
<div>{!! $user->bio !!}</div> {{-- VULNERÁVEL se usuário fornece bio --}}
Manipulação Segura de HTML
// Quando precisar permitir algum HTML, use uma abordagem de lista branca
use HTMLPurifier; // Requer: composer require ezyang/htmlpurifier
public function sanitizeHtml(string $dirty): string
{
$config = \HTMLPurifier_Config::createDefault();
$config->set('HTML.Allowed', 'p,b,i,a[href],ul,ol,li,br');
$config->set('URI.AllowedSchemes', ['http', 'https', 'mailto']);
$purifier = new \HTMLPurifier($config);
return $purifier->purify($dirty);
}
// No blade:
<div>{!! $sanitizedHtml !!}</div>
Content Security Policy (CSP)
// app/Http/Middleware/CspMiddleware.php
class CspMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
if (!app()->environment('production')) {
return $response;
}
$response->headers->set('Content-Security-Policy', implode('; ', [
"default-src 'self'",
"script-src 'self' 'nonce-" . base64_encode(random_bytes(16)) . "'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
"base-uri 'self'",
]));
return $response;
}
}
// Registrar em Kernel
protected $middleware = [
\App\Http\Middleware\CspMiddleware::class,
];
Segurança de API
Rate Limiting
// app/Http/Kernel.php ou specific route files
// Definir limitadores de taxa
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
RateLimiter::for('login', function (Request $request) {
return Limit::perMinute(5)->by($request->input('email') . '|' . $request->ip());
});
RateLimiter::for('register', function (Request $request) {
return Limit::perHour(3)->by($request->ip());
});
// Usar em rotas
Route::middleware(['throttle:api'])->group(function () {
Route::get('/user', function () { /* ... */ });
});
Route::post('/login', [LoginController::class, 'store'])
->middleware('throttle:login');
CORS (Cross-Origin Resource Sharing)
// config/cors.php
return [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => env('CORS_ALLOWED_ORIGINS')
? explode(',', env('CORS_ALLOWED_ORIGINS'))
: [env('APP_FRONTEND_URL', 'http://localhost:3000')],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true, // Necessário para Sanctum SPA
];
Headers de Resposta de Segurança
// app/Http/Middleware/SecurityHeadersMiddleware.php
class SecurityHeadersMiddleware
{
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('X-XSS-Protection', '0'); // Desativar legado
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$response->headers->set('Permissions-Policy', implode(', ', [
'camera=()',
'microphone=()',
'geolocation=()',
]));
// HSTS (Apenas HTTPS): Descomente após confirmar HTTPS
// $response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
return $response;
}
}
Validação de Entrada de API
// app/Http/Requests/StoreOrderRequest.php
class StoreOrderRequest extends FormRequest
{
public function rules(): array
{
return [
'items' => ['required', 'array', 'min:1', 'max:50'],
'items.*.product_id' => ['required', 'exists:products,id', 'integer'],
'items.*.quantity' => ['required', 'integer', 'min:1', 'max:100'],
'shipping_address' => ['required', 'string', 'max:500'],
'payment_method' => ['required', Rule::in(['credit_card', 'paypal', 'stripe'])],
'coupon_code' => ['nullable', 'string', 'max:20', 'exists:coupons,code'],
];
}
public function messages(): array
{
return [
'items.*.product_id.exists' => 'Um ou mais produtos são inválidos.',
'items.max' => 'Máximo de :max itens por pedido.',
];
}
protected function failedValidation(Validator $validator)
{
// Responder com JSON estruturado para APIs
throw new HttpResponseException(response()->json([
'message' => 'Erro de validação.',
'errors' => $validator->errors(),
], 422));
}
}
Configuração Segura de Deploy
Checklist de Ambiente
-
APP_DEBUGestáfalse -
APP_ENVestáproduction -
APP_KEYestá definido (gerado viaphp artisan key:generate) - HTTPS está forçado em produção
- Cookies de sessão são
secure,http_only,same_site=lax - .env nunca é commitado
- CORS está restrito a origens confiáveis
- Rate limiting está configurado para endpoints de API
- Headers de segurança estão aplicados via middleware
- Cache de configuração:
php artisan config:cache - Cache de rotas:
php artisan route:cache - Debug bar removida ou desativada em produção
- Logs de erro não são expostos ao usuário final
Configuração de Cache
# Otimizar para produção — execute como parte do script de deploy
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
# Limpar cache após atualizações de configuração
php artisan optimize:clear
Configuração de Banco de Dados
// config/database.php
// Usar variáveis de ambiente apenas, nunca valores fixos
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true, // Modo estrito do MySQL
'engine' => 'InnoDB', // Sempre usar InnoDB para integridade referencial
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => true,
]) : [],
],
Dependências e Atualizações
Monitoramento de Vulnerabilidades
# Verificar dependências em busca de vulnerabilidades conhecidas
composer audit
# Ou usar ferramentas SaaS:
# - GitHub Dependabot
# - GitLab Dependency Scan
# - Snyk
# - Laravel Forge (inclui segurança)
# Atualizar dependências regularmente
composer update --no-dev -W
Versões Suportadas do Laravel
- Sempre use a versão mais recente estável do Laravel
- Aplique patches de segurança prontamente
- Monitore o Laravel Security Advisories
- Teste atualizações em staging antes do deploy para produção