Resposta rápida: se sua aplicação Go autentica o navegador com cookie de sessão, proteja toda mutação (POST, PUT, PATCH, DELETE) com token CSRF (synchronizer token ou double-submit) e configure o cookie com Secure, HttpOnly e SameSite adequado. SameSite=Lax reduz o ataque, mas não basta sozinho. APIs que autenticam só por Authorization: Bearer (sem cookie automático) ficam fora do modelo clássico de CSRF; misturar cookie e Bearer na mesma rota exige decisão explícita.
CSRF (Cross-Site Request Forgery) continua aparecendo em reviews de segurança, checklists de deploy e entrevistas backend porque o bug não está na sintaxe de Go: está no contrato entre navegador, cookie e formulário. Este guia fecha a malha de autenticação do site junto com autenticação e autorização em APIs Go, OAuth 2.0 e OIDC, CORS em Go e o tutorial de segurança.
O que CSRF realmente explora
O navegador envia cookies de uma origem para aquela origem mesmo quando o request foi disparado por outra página. Se Alice está logada em app.exemplo.com e visita evil.example, um formulário oculto ou um fetch com credentials: 'include' pode tentar:
<form action="https://app.exemplo.com/transferir" method="POST">
<input type="hidden" name="para" value="conta-atacante">
<input type="hidden" name="valor" value="5000">
</form>
<script>document.forms[0].submit()</script>
O servidor vê um POST autenticado — o cookie de sessão viajou — e executa a transferência. O atacante não precisa ler a resposta; só precisa que a ação aconteça.
Três condições costumam coexistir quando CSRF é explorável:
- autenticação baseada em credencial automática (cookie de sessão);
- ação relevante sem confirmação adicional do usuário;
- ausência de segredo por request que o atacante não consegue prever.
XSS é outro problema: com XSS o atacante já roda JavaScript na sua origem e pode ler tokens. CSRF assume que a origem é saudável, mas o usuário visita um site malicioso. Por isso CSRF e XSS se reforçam — e por isso tokens CSRF em cookies legíveis por JS precisam de higiene rigorosa contra XSS.
SameSite: o que muda e o que não muda
Desde que os navegadores adotaram SameSite por padrão, muitos ataques “clássicos” ficaram mais difíceis. Em Go, ao gravar a sessão:
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: sessionID,
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
MaxAge: 86400,
})
| Valor | Comportamento típico | Uso comum |
|---|---|---|
Strict | Cookie quase só em navegações same-site | Apps internas sensíveis |
Lax | Envia em top-level GET cross-site; bloqueia POST cross-site na maioria dos casos | Sessão web padrão |
None | Envia cross-site; exige Secure | Embeds, alguns fluxos OAuth/iframe |
Lax é o ponto de partida sensato para a maioria das sessões. Ainda assim:
- usuários e proxies com navegadores antigos existem;
SameSite=Noneaparece em produtos com iframe ou autenticação cross-site;- algumas ações sensíveis merecem reautenticação, não só CSRF;
- bugs de configuração (
Secureausente em produção, domínio de cookie amplo demais) reabrem a superfície.
Regra prática: SameSite é mitigação; token CSRF é controle.
Synchronizer token: o padrão mais claro em apps Go
Fluxo:
- no login (ou na renderização do formulário), gere um token opaco com
crypto/rand; - guarde o token na sessão server-side;
- inclua o token no HTML (
<input type="hidden">) ou devolva-o em um endpoint para o frontend confiável; - em toda mutação autenticada, compare o token do body/header com o da sessão;
- rotacione o token após uso em ações críticas, se a UX permitir.
Esboço mínimo com net/http e sessão em memória (substitua por Redis/Postgres em produção):
package csrfutil
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"net/http"
)
const headerName = "X-CSRF-Token"
const formField = "csrf_token"
func NewToken() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}
func Validate(r *http.Request, expected string) bool {
got := r.Header.Get(headerName)
if got == "" {
got = r.FormValue(formField)
}
if expected == "" || got == "" {
return false
}
return subtle.ConstantTimeCompare([]byte(got), []byte(expected)) == 1
}
No handler do formulário:
func (s *Server) handleTransfer(w http.ResponseWriter, r *http.Request) {
sess := s.SessionFrom(r)
if sess == nil {
http.Error(w, "não autenticado", http.StatusUnauthorized)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, "form inválido", http.StatusBadRequest)
return
}
if !csrfutil.Validate(r, sess.CSRFToken) {
http.Error(w, "csrf inválido", http.StatusForbidden)
return
}
// executa a transferência...
}
Use subtle.ConstantTimeCompare para não vazar informação por tempo de resposta. Não logue o token. Não coloque o token na query string.
Double-submit cookie
Alternativa comum em SPAs:
- o servidor define um cookie
csrf(pode ser nãoHttpOnly, para o JS ler); - o JavaScript copia o valor para o header
X-CSRF-Token; - o servidor exige que cookie e header coincidam.
Vantagem: não precisa de sessão server-side só para CSRF. Riscos: subdomínios compartilhados, XSS lendo o cookie, e times que marcam o cookie CSRF como HttpOnly por reflexo — o que quebra o padrão. Se escolher double-submit, documente por que o cookie CSRF é legível e mantenha CSP, sanitização e escape em templates sob controle.
gorilla/csrf na prática
Para apps net/http e Chi, github.com/gorilla/csrf continua uma escolha pragmática:
package main
import (
"net/http"
"os"
"github.com/go-chi/chi/v5"
"github.com/gorilla/csrf"
)
func main() {
r := chi.NewRouter()
csrfMiddleware := csrf.Protect(
[]byte(os.Getenv("CSRF_AUTH_KEY")), // 32 bytes aleatórios
csrf.Secure(true),
csrf.Path("/"),
csrf.SameSite(csrf.SameSiteLaxMode),
csrf.ErrorHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "forbidden", http.StatusForbidden)
})),
)
r.Group(func(r chi.Router) {
r.Use(csrfMiddleware)
r.Get("/form", formHandler)
r.Post("/form", submitHandler)
})
http.ListenAndServe(":8080", r)
}
No template:
<form method="POST" action="/form">
<input type="hidden" name="gorilla.csrf.Token" value="{{ csrfField }}">
<!-- campos da ação -->
<button type="submit">Confirmar</button>
</form>
Em requests AJAX/HTMX, envie o header X-CSRF-Token com o valor exposto pelo middleware (meta tag ou cookie auxiliar, conforme a configuração). Combine com o guia de middleware em Go para ordenar autenticação, CSRF, request ID e timeout.
Formulários, HTMX e templ
Aplicações com templ + HTMX mudam o transporte, não a ameaça. Um hx-post autenticado por cookie é uma mutação como qualquer outra:
- inclua o token CSRF em cada partial que dispara escrita;
- ou configure
hx-headersglobais com o token da página; - valide no servidor sempre — nunca confie só no fato de a UI “esconder” o botão.
Para uploads multipart, o token pode ir em campo hidden no mesmo form; veja também upload de arquivos em Go para limites de tamanho e content-type.
APIs JSON, SPA e mobile
Decisão rápida:
| Superfície | Autenticação | CSRF necessário? |
|---|---|---|
| HTML server-rendered / HTMX | cookie de sessão | Sim |
| SPA same-site com cookie | cookie de sessão | Sim (token ou double-submit) |
| SPA/mobile com Bearer no header | Authorization | Em geral não |
| API pública sem credencial | nenhuma | Não |
| Híbrido cookie + Bearer na mesma rota | misto | Trate como cookie: Sim, ou separe rotas |
Se o frontend guarda o access token em localStorage, você trocou CSRF por um risco maior de XSS. O desenho mais seguro para apps browser costuma ser sessão opaca em cookie HttpOnly + CSRF, ou BFF que segura tokens como no guia de OIDC.
CORS não é CSRF
Política CORS restritiva impede que evil.example leia a resposta de fetch para a sua API. Ela não impede, por si só, que o navegador dispare um POST “simple request” ou um submit de formulário com cookies. Por isso o guia de CORS e este se complementam: CORS para leitura cross-origin controlada; CSRF para mutações autenticadas por cookie.
Evite Access-Control-Allow-Origin: * com credenciais. Evite refletir Origin sem allowlist. Em pré-flights, não trate OPTIONS como atalho de autenticação.
Erros comuns em produção
Cookie de sessão sem Secure / HttpOnly
Sem HttpOnly, XSS rouba a sessão. Sem Secure, a sessão viaja em HTTP claro. Sem SameSite, o navegador fica mais permissivo em contextos cross-site.
Token CSRF na query string
URLs vazam em logs, Referer, analytics e histórico. Use body ou header.
Validar Origin/Referer como única defesa
Útil como sinal auxiliar (especialmente em APIs), frágil sozinho: proxies removem headers, browsers divergem, e políticas de privacidade mudam. Prefira token + SameSite; use Origin como defesa em profundidade.
Exceção ampla demais no middleware
Pular CSRF em “webhooks” ou “health” é ok; pular em /api/* inteiro porque “é JSON” é o caminho clássico do incidente. Liste exceções por rota e revise no PR.
Chave CSRF_AUTH_KEY fraca ou commitada
Gere 32 bytes com openssl rand -base64 32, injete por secret manager e rotacione com plano. O mesmo rigor de configuração com Viper vale aqui.
Confundir idempotência com CSRF
Idempotência e retry evitam efeito duplicado de um request legítimo repetido. CSRF evita que um request forjado seja aceito como legítimo. Você precisa dos dois em APIs financeiras e de conta.
Como testar
Cubra pelo menos:
POSTautenticado sem token →403;- token inválido ou de outra sessão →
403; - token válido →
2xxe efeito colateral esperado; - método seguro (
GET) não exige token (e não muta estado); - cookie de sessão com flags corretas no Set-Cookie;
- request cross-site simulado sem token bloqueado.
func TestTransferRequiresCSRF(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/transferir", strings.NewReader("para=x&valor=1"))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
// anexa cookie de sessão de teste, mas sem csrf_token
rr := httptest.NewRecorder()
ts.Server.ServeHTTP(rr, req)
if rr.Code != http.StatusForbidden {
t.Fatalf("got %d", rr.Code)
}
}
Use httptest como no guia de mocks e httptest. Em integração, suba a app com Testcontainers se a sessão depender de Redis/Postgres. Rode go test -race e govulncheck no CI.
Checklist de produção
- Sessão usa cookie
Secure+HttpOnly+SameSiteexplícito. - Toda rota mutável autenticada por cookie valida CSRF.
- Token vem de
crypto/rand(ou biblioteca madura) e comparação é constant-time. - Token não aparece em URL, logs ou mensagens de erro.
- Templates/HTMX incluem o campo ou header em todos os forms de escrita.
- Exceções de middleware são poucas e nomeadas (webhook assinado, health).
- CORS allowlist não usa
*com credenciais. - Ações destrutivas críticas pedem reautenticação ou confirmação extra.
- Chave CSRF está em secret, não no repositório.
- Testes cobrem ausência/invalidade do token.
- Rate limiting e authz por recurso existem além do CSRF.
- Logout invalida a sessão server-side (graceful shutdown não esquece sessões em voo).
Perguntas frequentes
SameSite=Lax elimina a necessidade de token CSRF em Go?
Não. Lax reduz CSRF em navegadores modernos, mas não cobre todos os clientes nem todos os desenhos (SameSite=None, autenticação híbrida, bugs de cookie). Use token em mutações autenticadas por cookie.
API REST com Authorization Bearer precisa de CSRF?
Se o navegador não envia a credencial automaticamente, o modelo clássico de CSRF não se aplica. Se a mesma rota também aceita cookie de sessão, proteja como superfície browser.
Synchronizer token ou double-submit?
Para apps Go com sessão server-side e HTML/HTMX, synchronizer token é o mais simples de auditar. Double-submit encaixa melhor em SPAs que já leem um cookie auxiliar — com disciplina forte contra XSS.
gorilla/csrf ainda vale em 2026?
Sim para net/http/Chi e fluxos de formulário. Em Gin/Echo/Fiber, use o equivalente do framework ou adapte o middleware; a regra de negócio é a mesma.
CSRF e CORS são a mesma coisa?
Não. CORS regula leitura cross-origin via JS; CSRF regula mutações forjadas com credenciais automáticas. Você normalmente configura os dois.
Próximos passos
Inventarie suas rotas mutáveis e marque quais autenticam por cookie. Se alguma mutação importante ainda depende só de “o usuário está logado”, adicione token CSRF e revise as flags do cookie no mesmo PR. Em seguida, alinhe CORS, autenticação e autorização para que cada camada tenha uma responsabilidade clara.
Para continuar a malha de produção: middleware HTTP, autenticação e autorização, OAuth/OIDC, CORS, rate limiting e o tutorial de segurança em Go. Se estiver montando portfólio, um form autenticado com CSRF testado é um sinal forte de maturidade backend — veja ideias em projetos Go para portfólio.