CSRF protection
Como funciona @csrf, X-CSRF-TOKEN, exempting routes.
O CSRF deste app é o nativo do Laravel — não existe um mecanismo
paralelo. O middleware Illuminate\Foundation\Http\Middleware\ValidateCsrfToken
(sucessor do clássico VerifyCsrfToken) já vem no grupo web e
valida o token contra session()->token() — o mesmo token que o helper
nativo csrf_token() e a diretiva @csrf usam. O ponto de
configuração único é bootstrap/app.php.
Mad\Web\Csrf no código — ela não é o mecanismo
atual. Faz parte de uma stack paralela de helpers (Mad\Web\*:
Session, ViewResponse, AuthHelper,
BladeDirectives) que neste app full-Laravel fica inerte para CSRF — os
globais view()/redirect() do Mad\Web são
function_exists()-guardados e os do Laravel carregam primeiro, então nada
referencia Mad\Web\Csrf fora do próprio arquivo. O @csrf que você vê em
resources/views/**.blade.php (como o desta própria página) é a diretiva
nativa do Blade do Laravel, compilada pelo Illuminate\View\Compilers\BladeCompiler
de verdade.
Como funciona, passo a passo
1. GET /public/ged/{token}
→ Sessão Laravel já tem (ou gera) um token: session()->token()
→ Layout injeta <meta name="csrf-token" content="{{ csrf_token() }}">
→ Form usa @csrf, que emite <input type="hidden" name="_token" value="...">
2. POST /public/ged/{token}
→ Browser envia _token=... no body (ou X-CSRF-TOKEN no header, via fetch)
3. ValidateCsrfToken (grupo web, roda em toda rota de routes/web.php):
- Lê o token apresentado (body _token / header X-CSRF-TOKEN / cookie XSRF-TOKEN)
- Compara com session()->token()
- Match → segue para o controller
- Sem match → 419 (Page Expired)
Forms server-rendered tradicionais
Exemplo real deste repositório — o form de senha do link público do GED:
<!-- resources/views/ged/public/password.blade.php -->
<form method="POST" action="{{ route('ged.public.unlock', ['token' => $token]) }}">
@csrf
<label for="password">{{ __('ged.password_optional') }}</label>
<input type="password" id="password" name="password" required>
<button type="submit">{{ __('ged.open') }}</button>
</form>
Repare: é uma rota totalmente anônima (sem mad.auth) e mesmo
assim tem CSRF — porque CSRF protege contra forjar uma requisição em NOME de quem quer que
esteja com aquela sessão (com ou sem login), não é exclusivo de área logada.
AJAX / fetch e o ciclo reativo (MadWire)
O mesmo token do @csrf serve para chamadas JSON. O layout do portal injeta o
meta tag uma vez por full-load; o mad-livewire.js lê e envia em todo POST
reativo:
<!-- resources/views/public/docs-fw/layout.blade.php -->
<meta name="csrf-token" content="{{ csrf_token() }}">
// packages/mad-framework/assets/mad-livewire.js
const _csrfMeta = document.querySelector('meta[name="csrf-token"]');
const _csrfToken = _csrfMeta ? _csrfMeta.content : '';
if (_csrfToken) _headers['X-CSRF-TOKEN'] = _csrfToken;
Tanto POST /app/_mad-wire (admin) quanto POST /public/_mad-wire
(portal público, usado pela navegação reativa desta própria documentação) estão dentro do
grupo web e passam pelo ValidateCsrfToken nativo
normalmente — não há lógica de CSRF manual nesses controllers.
POST do header / notchbar — casco estático chamando PHP
Botões e combos do shell (trocar empresa/unidade, recarregar, sair) não passam pelo ciclo
do MadWire: são fetch manuais a partir do casco. Duas regras valem para eles:
-
A URL vem do server, montada com
MadRoutes::urlFor($class, $method, $params)e emitida comodata-urlno elemento — o mapa outbound de rotas não é exportado para o client, e chamar a classe crua pelo nome cai fora da allowlist (404). -
O POST precisa levar o
X-CSRF-TOKENexplicitamente: é umfetchcru, ninguém injeta o header por você.
// PHP (ShellViewModel / control do notchbar)
$url = \Mad\Routing\MadRoutes::urlFor('ChangeTenantForm', 'onChange');
// → <button data-url="/app/trocar-empresa/onChange">
const res = await fetch(btn.dataset.url, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'X-Requested-With': 'XMLHttpRequest',
},
body: new FormData(form),
});
await Mad.applyOps(await res.json()); // aplica o MadResponse no DOM
Sem o header, a rota (que está no grupo web como qualquer outra do
routes/web.php) devolve 419. Ver
Rotas públicas para o contrato completo do
urlFor().
MadCsrf — defesa em profundidade no endpoint do admin
Além do middleware nativo, MadAppController::wire() (só o endpoint
admin, /app/_mad-wire) chama
Mad\Security\MadCsrf::validateWire() antes de processar a ação:
// packages/mad-framework/src/mad/security/MadCsrf.php
class MadCsrf
{
/** Token CSRF da sessão Laravel — o MESMO que csrf_token(). */
public static function token(): string
{
$session = app('session');
if (!$session->token()) {
$session->regenerateToken();
}
return (string) $session->token();
}
/** Lê o token do client: header X-CSRF-TOKEN, body _token/mad_csrf, header X-XSRF-TOKEN. */
public static function readFromRequest(): string { /* ... */ }
public static function check(?string $provided = null): bool { /* compara com hash_equals */ }
/** Só EXIGE token para sessão LOGADA — fluxo anônimo (login/cadastro) passa sem token,
* o estado criptografado do MadWire já cobre esse caso. */
public static function validateWire(): bool
{
if (!session('logged')) {
return true;
}
return self::check();
}
}
MadCsrf::token() é literalmente session()->token() do Laravel
— "CSRF unificado" quer dizer isso: um único token, uma única fonte de verdade. O
validateWire() é só uma checagem extra explícita no controller do admin, não
uma segunda implementação.
Excluindo rotas do CSRF (webhooks, integrações stateless)
O ponto de configuração é $middleware->validateCsrfTokens(except: [...])
em bootstrap/app.php. Hoje as únicas exceções são os dois grupos de iframe
cross-origin do Copilot embed e do Mad Agent Command Center:
// bootstrap/app.php
$middleware->validateCsrfTokens(except: [
'embed/v1/*',
'agent-console/v1/*',
]);
Authorization: Bearer
(token MCP, validado por McpManifestAuthMiddleware) em todo request —
CSRF protege sessão-cookie, e esses fluxos simplesmente não usam cookie de sessão para
autenticar. Nunca exponha uma rota mutável sem algum mecanismo de auth.
A camada /api/* não precisa de exceção
As rotas de routes/api.php (API pública stateless, atrás do middleware
mad.api) não aparecem na lista de except — e nem
precisam: elas não estão no grupo web. O withRouting(api: …) as
registra no grupo api do Laravel, que tem só SubstituteBindings:
sem StartSession, portanto sem ValidateCsrfToken.
// bootstrap/app.php
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php', // grupo `api`: sem sessão, sem CSRF
...
)
Ali a autenticação é inteiramente o Authorization: Bearer mad_api_… resolvido
pelo mad.api (token → unit → tenant → banco), com abilities declaradas por rota
— mesmo princípio das exceções acima: sem cookie de sessão, CSRF não se aplica. Detalhes em
REST API — Autenticação e
Middleware stack. Endpoints
/app/api/* internos (atrás de mad.auth, dentro do
routes/web.php) continuam no grupo web — esses exigem
o token CSRF normalmente.
Fontes aceitas do token (ordem de leitura)
| # | Fonte | Uso típico |
|---|---|---|
| 1 | Header X-CSRF-TOKEN | mad-livewire.js e qualquer fetch/AJAX manual |
| 2 | Body _token | Forms HTML tradicionais via @csrf |
| 3 | Cookie XSRF-TOKEN → header X-XSRF-TOKEN | Clients HTTP que respeitam o cookie automático do Laravel |
Erro comum — 419 Page Expired
<!-- ERRADO: form sem @csrf — POST sempre falha com 419 -->
<form method="POST" action="/public/ged/{{ $token }}">
<input type="password" name="password">
</form>
<!-- CERTO -->
<form method="POST" action="/public/ged/{{ $token }}">
@csrf
<input type="password" name="password">
</form>
Outra causa comum: sessão expirada/regenerada (ex: depois de login) deixando o token do meta tag antigo na página aberta há tempo — nesse caso o fix é recarregar a página antes de submeter, não desabilitar CSRF.
Helpers e diretivas — todos nativos do Laravel
| Helper/diretiva | Retorno | Uso |
|---|---|---|
csrf_token() | string | Token atual da sessão (gera se ausente). |
csrf_field() | string | HTML do <input hidden> — o que @csrf emite por baixo. |
@csrf | diretiva Blade | Dentro de <form method="POST">. |
Mad\Security\MadCsrf::token() | string | Mesmo token, usado por ShellViewModel/MadAppController quando o código já está no contexto Mad\*. |
Próximos passos
- Middleware stack — onde
ValidateCsrfTokenentra no pipeline. - Rotas públicas — declarando rotas que herdam o grupo
web(e portanto CSRF) automaticamente. - REST API — Autenticação — endpoints internos usam sessão (
mad.auth) e portanto CSRF; a API pública/api/*(mad.api) é stateless e autentica por Bearer, sem CSRF.