← Voltar para o blog

CSRF em Go: SameSite, Tokens e APIs Seguras

Proteja formulários e cookies em Go contra CSRF: SameSite, token sincronizador, double-submit, gorilla/csrf, SPA com Bearer e checklist completo de produção.

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:

  1. autenticação baseada em credencial automática (cookie de sessão);
  2. ação relevante sem confirmação adicional do usuário;
  3. 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,
})
ValorComportamento típicoUso comum
StrictCookie quase só em navegações same-siteApps internas sensíveis
LaxEnvia em top-level GET cross-site; bloqueia POST cross-site na maioria dos casosSessão web padrão
NoneEnvia cross-site; exige SecureEmbeds, 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=None aparece 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 (Secure ausente 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:

  1. no login (ou na renderização do formulário), gere um token opaco com crypto/rand;
  2. guarde o token na sessão server-side;
  3. inclua o token no HTML (<input type="hidden">) ou devolva-o em um endpoint para o frontend confiável;
  4. em toda mutação autenticada, compare o token do body/header com o da sessão;
  5. 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.

Alternativa comum em SPAs:

  1. o servidor define um cookie csrf (pode ser não HttpOnly, para o JS ler);
  2. o JavaScript copia o valor para o header X-CSRF-Token;
  3. 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-headers globais 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ícieAutenticaçãoCSRF necessário?
HTML server-rendered / HTMXcookie de sessãoSim
SPA same-site com cookiecookie de sessãoSim (token ou double-submit)
SPA/mobile com Bearer no headerAuthorizationEm geral não
API pública sem credencialnenhumaNão
Híbrido cookie + Bearer na mesma rotamistoTrate 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

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:

  1. POST autenticado sem token → 403;
  2. token inválido ou de outra sessão → 403;
  3. token válido → 2xx e efeito colateral esperado;
  4. método seguro (GET) não exige token (e não muta estado);
  5. cookie de sessão com flags corretas no Set-Cookie;
  6. 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 + SameSite explí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.