8.2 CSRF (Cross-Site Request Forgery)
Resumo
CSRF (Cross-Site Request Forgery) — ataque em que o atacante faz um usuário autenticado executar uma ação indesejada em outro site.
Proteção: o Laravel valida o token CSRF sozinho pelo middleware
VerifyCsrfToken. A diretiva Blade@csrfcoloca um campo hidden com o token. No AJAX, use o headerX-CSRF-TOKEN.Importante: cookies SameSite dão uma camada extra. API com tokens (Sanctum) não precisa de CSRF.
Conteúdo
O que é
O que é: CSRF — falsificação de request entre sites. O atacante faz o usuário executar uma ação indesejada no site em que ele está autenticado.
Como funciona o ataque:
- O usuário está logado em site.com
- Abre evil.com
- evil.com manda um POST para site.com
- A request roda no nome do usuário
Como funciona
Exemplo de ataque CSRF:
<!-- evil.com -->
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker">
<input type="hidden" name="amount" value="10000">
</form>
<script>
document.forms[0].submit(); // Envia sozinho
</script>
<!-- Se o usuário estiver logado no bank.com,
a transferência roda sem ele perceber -->Proteção com token CSRF:
// ❌ Código VULNERÁVEL (sem CSRF)
Route::post('/transfer', function (Request $request) {
$user = auth()->user();
$user->balance -= $request->input('amount');
// A transferência roda
});
// ✅ PROTEÇÃO: token CSRF (Laravel por padrão)
// No form
<form method="POST" action="/transfer">
@csrf <!-- Gera o campo hidden com o token -->
<input type="number" name="amount">
<button type="submit">Transferir</button>
</form>
// O Laravel valida o token sozinho pelo middleware
// app/Http/Kernel.php
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\VerifyCsrfToken::class, // Proteção CSRF
],
];Token CSRF no JavaScript:
// O Laravel coloca o token no meta tag sozinho
<meta name="csrf-token" content="{{ csrf_token() }}">
// O Axios coloca no header sozinho
// resources/js/bootstrap.js
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = document
.querySelector('meta[name="csrf-token"]')
.getAttribute('content');
// Fetch na mão
fetch('/api/endpoint', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});Quando usar
CSRF precisa para:
- ✅ Requests POST, PUT, DELETE
- ✅ Mudança de dados (transferência, compra, exclusão)
- ✅ Formulários web
CSRF NÃO precisa para:
- ❌ Requests GET (só leitura)
- ❌ API com tokens (Sanctum, Passport)
- ❌ API stateless
Exemplo prático
Exceção na checagem CSRF:
// app/Http/Middleware/VerifyCsrfToken.php
class VerifyCsrfToken extends Middleware
{
// Fora da checagem CSRF
protected $except = [
'webhook/*', // Webhooks de serviços externos
'api/*', // API endpoints (usam tokens)
];
}CSRF no AJAX:
// Componente Vue.js
export default {
methods: {
async submitForm() {
try {
const response = await axios.post('/api/posts', {
title: this.title,
body: this.body,
});
// Token CSRF entra sozinho pelo Axios
} catch (error) {
if (error.response.status === 419) {
alert('Token CSRF não bate. Recarregue a página.');
}
}
}
}
}Cookies SameSite (proteção extra):
// config/session.php
'same_site' => 'lax', // Ou 'strict'
// Atributos SameSite:
// - 'strict' — cookie não vai em request de outro site (proteção forte)
// - 'lax' — cookie só vai em GET (equilíbrio)
// - 'none' — cookie vai sempre (precisa para iframe)Double Submit Cookie (alternativa):
// Método alternativo de proteção CSRF
class DoubleSubmitCsrfMiddleware
{
public function handle($request, Closure $next)
{
if ($request->isMethod('POST')) {
$cookieToken = $request->cookie('csrf_token');
$headerToken = $request->header('X-CSRF-TOKEN');
if ($cookieToken !== $headerToken) {
abort(419, 'Token CSRF não bate');
}
}
return $next($request);
}
}Sanctum na API (sem CSRF):
// API usa tokens no lugar de sessões
Route::middleware('auth:sanctum')->post('/posts', function (Request $request) {
// CSRF não precisa (stateless)
return Post::create($request->all());
});
// O cliente manda Bearer token
fetch('/api/posts', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token, // No lugar do CSRF
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});Checagem de referer (extra):
class CheckRefererMiddleware
{
public function handle($request, Closure $next)
{
$referer = $request->headers->get('referer');
if ($referer && !str_starts_with($referer, config('app.url'))) {
abort(403, 'Referer inválido');
}
return $next($request);
}
}Testando CSRF:
// tests/Feature/CsrfTest.php
class CsrfTest extends TestCase
{
public function test_post_without_csrf_token_fails(): void
{
$response = $this->post('/posts', [
'title' => 'Test',
'body' => 'Content',
]);
$response->assertStatus(419); // Token CSRF não bate
}
public function test_post_with_csrf_token_succeeds(): void
{
$response = $this->post('/posts', [
'title' => 'Test',
'body' => 'Content',
'_token' => csrf_token(),
]);
$response->assertStatus(302);
}
public function test_csrf_token_regenerates_on_login(): void
{
$oldToken = csrf_token();
$this->post('/login', [
'email' => 'test@example.com',
'password' => 'password',
]);
$newToken = csrf_token();
$this->assertNotEquals($oldToken, $newToken);
}
}CSRF no SPA (Single Page Application):
// routes/web.php
Route::get('/sanctum/csrf-cookie', function () {
// Inicializa o cookie CSRF para o SPA
return response()->noContent();
});
// JavaScript (primeira request)
await axios.get('/sanctum/csrf-cookie');
// Agora toda request vai com CSRF
await axios.post('/api/posts', data);Na entrevista
"CSRF é falsificação de request de outro site. O Laravel protege com token CSRF (@csrf no form). O middleware VerifyCsrfToken checa o token em POST/PUT/DELETE. Header X-CSRF-TOKEN no AJAX (Axios coloca sozinho). Cookie SameSite é proteção extra. API com token (Sanctum) não precisa de CSRF (stateless). Exceção pelo $except no VerifyCsrfToken. Erro 419 se o token não bater. O token regenera no login."
Exercícios práticos
Exercício 1: Corrija o erro CSRF no AJAX
Enunciado: Você tem um componente Vue que manda POST e recebe 419. Corrija.
// PostForm.vue
export default {
data() {
return {
title: '',
body: '',
}
},
methods: {
async submit() {
const response = await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify({
title: this.title,
body: this.body,
}),
});
}
}
}Solução
// Solução 1: Colocar o token CSRF no header
export default {
data() {
return {
title: '',
body: '',
}
},
methods: {
async submit() {
const token = document.querySelector('meta[name="csrf-token"]').content;
const response = await fetch('/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': token,
},
body: JSON.stringify({
title: this.title,
body: this.body,
}),
});
}
}
}
// No layout.blade.php (adicionar o meta tag)
<head>
<meta name="csrf-token" content="{{ csrf_token() }}">
</head>
// Solução 2: Usar Axios (coloca o token sozinho)
// resources/js/bootstrap.js
import axios from 'axios';
window.axios = axios;
window.axios.defaults.headers.common['X-CSRF-TOKEN'] =
document.querySelector('meta[name="csrf-token"]').content;
// No componente
export default {
methods: {
async submit() {
const response = await axios.post('/api/posts', {
title: this.title,
body: this.body,
});
}
}
}Exercício 2: Configure exceções CSRF para webhook
Enunciado: Você tem um endpoint de webhook do Stripe que não passa na checagem CSRF. Configure a exceção.
Solução
// app/Http/Middleware/VerifyCsrfToken.php
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* URIs fora da checagem CSRF
*/
protected $except = [
'webhooks/stripe', // Endpoint específico
'webhooks/*', // Todos os webhooks
'api/*', // Todas as rotas de API (se usam tokens)
];
}
// routes/web.php
Route::post('/webhooks/stripe', [StripeWebhookController::class, 'handle'])
->name('webhooks.stripe');
// StripeWebhookController
class StripeWebhookController extends Controller
{
public function handle(Request $request)
{
// Valida a assinatura do Stripe no lugar do CSRF
$signature = $request->header('Stripe-Signature');
$webhookSecret = config('services.stripe.webhook_secret');
try {
$event = \Stripe\Webhook::constructEvent(
$request->getContent(),
$signature,
$webhookSecret
);
// Processa o webhook
match ($event->type) {
'payment_intent.succeeded' => $this->handlePaymentSucceeded($event),
'customer.subscription.deleted' => $this->handleSubscriptionDeleted($event),
default => null,
};
return response()->json(['status' => 'success']);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 400);
}
}
}Exercício 3: Implemente CSRF no SPA
Enunciado: Configure proteção CSRF para Single Page Application com Laravel Sanctum.
Solução
// 1. Config do Sanctum
// config/sanctum.php
return [
'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) : ''
))),
'middleware' => [
'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,
],
];
// 2. Rota para inicializar o cookie CSRF
// routes/web.php
Route::get('/sanctum/csrf-cookie', function () {
return response()->noContent();
});
// 3. Setup do Axios no SPA
// src/api/client.js
import axios from 'axios';
const api = axios.create({
baseURL: 'http://localhost:8000',
withCredentials: true, // Importante! Mandar cookies
});
// Inicializa CSRF antes da primeira request
let csrfInitialized = false;
api.interceptors.request.use(async (config) => {
if (!csrfInitialized && config.method !== 'get') {
await axios.get('http://localhost:8000/sanctum/csrf-cookie', {
withCredentials: true,
});
csrfInitialized = true;
}
return config;
});
export default api;
// 4. Uso no componente
// src/components/LoginForm.vue
import api from '@/api/client';
export default {
methods: {
async login() {
try {
// Cookie CSRF entra sozinho
const response = await api.post('/api/login', {
email: this.email,
password: this.password,
});
console.log('Logado:', response.data);
} catch (error) {
console.error('Login falhou:', error);
}
}
}
}
// 5. config/cors.php
return [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:3000')],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true, // Importante!
];Parte do PHP/Laravel Interview Handbook | Feito com ❤️ pela equipe CodeMate