Melhores Práticas de Segurança para Laravel
Diretrizes abrangentes de segurança para aplicações Laravel para proteção contra vulnerabilidades comuns.
Quando Ativar
- Configurando autenticação e autorização no Laravel (Sanctum, Passport, Jetstream, Breeze)
- Implementando papéis de usuário, permissões e políticas
- Configurando definiçõ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 consultas Eloquent e migrações seguras
Configuração de Produção
Definiçõ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á definida. 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 no lugar
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 (balanceadores de carga)
// Use intervalos de IP específicos — * confia em todos, permitindo falsificação de 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 com 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 Senha
// 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);
// ...
}
Políticas
// 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 no 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 em 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 o papel: ' . $role);
}
return $next($request);
}
}
// Registrar em Kernel
protected $routeMiddleware = [
'role' => \App\Http\Middleware\CheckRole::class,
];
// Uso em rotas
Route::middleware(['auth', 'role:admin'])->group(function () {
Route::get('/admin', [AdminController::class, 'index']);
});
Segurança do 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 de atributos fillable
final class User extends Authenticatable
{
protected $fillable = [
'name',
'email',
'phone',
'avatar',
];
// NUNCA adicione 'role', 'is_admin', 'is_verified' aqui
}
// BOM: Controlar explicitamente 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 diretamente com dados da requisição
User::create($request->all()); // VULNERÁVEL a atribuição em massa!
// MELHOR: Usar DTOs para criação
$user = User::create($request->validated()); // Apenas campos validados
Prevenção de Injeção SQL
// BOM: Eloquent parametriza consultas 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 strings
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 boolean previne injeção de string
'settings' => 'array', // Automatizamente 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',
];
// Anexar apenas atributos computados seguros
protected $appends = ['full_name']; // seguro
// NUNCA anexar 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
// 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
{
// Exclua apenas 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 Sanctom stateful precisam de CSRF.
// Exclua apenas rotas específicas de webhook/endpoint sem estado.
];
}
CSRF com JavaScript
<meta name="csrf-token" content="{{ csrf_token() }}">
<script>
// Exemplo com Axios (Laravel vem com 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
{{-- SEGURO: Escape automático 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 o usuário fornecer bio --}}
Manipulação Segura de HTML
// Quando você 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> {{-- Use apenas após sanitização --}}
Segurança de API
Limitação de Taxa
// app/Http/Kernel.php
protected $middlewareGroups = [
'api' => [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
// Limitação personalizada por rota
Route::middleware('throttle:10,1')->group(function () {
Route::post('/login', [AuthController::class, 'login']);
Route::post('/password/reset', [PasswordResetController::class, 'sendResetLinkEmail']);
});
// Limitação de taxa dinâmica baseada no usuário
Route::middleware('throttle:100,1')->group(function () {
Route::get('/users', [UserController::class, 'index']);
Route::post('/posts', [PostController::class, 'store']);
});
Validação de Entrada
// BOM: Sempre validar entrada da API
// app/Http/Requests/StorePostRequest.php
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255', 'not_regex:/<[^>]*>/'],
'content' => ['required', 'string', 'max:10000'],
'category_id' => ['required', 'exists:categories,id'],
'tags' => ['array', 'max:5'],
'tags.*' => ['exists:tags,id'],
];
}
// BOM: Sanitizar saída de API
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
// Excluir atributos sensíveis como 'role' ou 'is_admin'
];
}
}
Autenticação de Webhook
// BOM: Verificar assinatura de webhook
// Exemplo com Stripe
$payload = $request->getContent();
$sig_header = $request->header('Stripe-Signature');
$endpoint_secret = config('services.stripe.webhook_secret');
try {
$event = \Stripe\Webhook::constructEvent(
$payload, $sig_header, $endpoint_secret
);
} catch(\UnexpectedValueException $e) {
// Payload inválido
abort(400, 'Payload inválido');
} catch(\Stripe\Exception\SignatureVerificationException $e) {
// Assinatura inválida
abort(400, 'Assinatura inválida');
}
// BOM: Validar IP do remetente do webhook
$allowedIps = ['192.168.1.1', '10.0.0.1']; // Ips conhecidos do serviço de webhook
if (!in_array($request->ip(), $allowedIps)) {
abort(403, 'IP não autorizado');
}
Sanctum com SPA
// 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) : ''
)));
// Certifique-se de que o SPA inclua o header 'Accept: application/json'
// para obter respostas de erro JSON
// BOM: Login de SPA
public function login(Request $request): JsonResponse
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
]);
if (Auth::attempt($request->only('email', 'password'), $request->boolean('remember'))) {
$request->session()->regenerate();
return response()->json([
'user' => new UserResource(Auth::user()),
'message' => 'Login bem-sucedido',
]);
}
return response()->json([
'message' => 'As credenciais fornecidas não correspondem aos nossos registros.',
], 401);
}
Segurança de Implantação
Variáveis de Ambiente
# .env.production
export APP_ENV=production
export APP_DEBUG=false
export APP_KEY=base64:...
export DB_PASSWORD=seu_senha_segura
export DB_CONNECTION=mysql
export DB_HOST=127.0.0.1
export DB_PORT=3306
export DB_DATABASE=seu_banco
export DB_USERNAME=seu_usuario
export SESSION_DRIVER=database
export SESSION_SECURE_COOKIE=true
export SESSION_LIFETIME=120
export SANCTUM_STATEFUL_DOMAINS=seudominio.com
export SANCTUM_TOKEN_PREFIX=seu_prefixo_
Configurações do Servidor
# Exemplo de configuração Nginx para segurança
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name seudominio.com;
root /var/www/html/public;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
Ferramentas de Verificação de Segurança
# Usar Laravel Security Checker
composer require --dev enlightn/laravel-security-checker
# Executar verificação de segurança
php artisan security:check
# Usar Enlightn para verificação abrangente
composer require --dev enlightn/enlightn
# Publicar configuração
php artisan vendor:publish --tag=enlightn
# Executar verificação
php artisan enlightn
# Verificar dependências com Composer Audit
composer audit
Resumo
A segurança de uma aplicação Laravel requer atenção em múltiplas camadas:
- Configuração de Produção: Desabilitar debug, forçar HTTPS, usar cookies seguros
- Autenticação: Usar Sanctum/Passport com tokens de API, validar senhas com requisitos de força
- Autorização: Implementar Gates e Políticas para controle de acesso granular
- Segurança do Eloquent: Proteger contra atribuição em massa, usar consultas parametrizadas
- CSRF: Incluir token CSRF em formulários e requisições AJAX
- XSS: Usar escape automático do Blade, sanitizar HTML quando necessário
- Segurança de API: Limitar taxa, validar entrada, autenticar webhooks
- **Implantaçã”: Configurar servidor web com cabeçalhos de segurança, usar variáveis de ambiente
Sempre manter dependências atualizadas e executar verificações de segurança regulares. Usar ferramentas como Enlightn e Laravel Security Checker para automatizar a detecção de vulnerabilidades.