---
title: "CSRF em Go: SameSite, Tokens e APIs Seguras"
url: "https://golang.com.br/blog/csrf-go-samesite-token-producao/"
markdown_url: "https://golang.com.br/blog/csrf-go-samesite-token-producao.MD"
description: "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."
date: "2026-08-25"
author: "Golang Brasil"
---

# 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](/blog/autenticacao-autorizacao-go-apis/), [OAuth 2.0 e OIDC](/blog/oauth2-oidc-go-login-google-keycloak/), [CORS em Go](/blog/cors-go-net-http-chi-gin/) e o tutorial de [segurança](/tutoriais/go-security/).

## 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:

```html
<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:

```go
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=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):

```go
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:

```go
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:

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](/blog/templ-go-componentes-html-htmx/) sob controle.

## gorilla/csrf na prática

Para apps `net/http` e Chi, `github.com/gorilla/csrf` continua uma escolha pragmática:

```go
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:

```html
<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](/blog/middleware-go-net-http-chi-gin-producao/) para ordenar autenticação, CSRF, request ID e timeout.

## Formulários, HTMX e templ

Aplicações com [templ + HTMX](/blog/templ-go-componentes-html-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](/blog/upload-arquivos-go-multipart-s3-seguranca/) 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](/blog/oauth2-oidc-go-login-google-keycloak/).

## 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](/blog/cors-go-net-http-chi-gin/) 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](/blog/configuracao-go-viper-variaveis-ambiente/) vale aqui.

### Confundir idempotência com CSRF

[Idempotência e retry](/blog/idempotencia-retry-dlq-go/) 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.

```go
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](/blog/mocks-go-testify-gomock-fakes-httptest/). Em integração, suba a app com [Testcontainers](/blog/go-testcontainers-testes-integracao-containers/) se a sessão depender de Redis/Postgres. Rode `go test -race` e [govulncheck](/blog/govulncheck-go-vulnerabilidades-dependencias/) 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](/blog/rate-limiting-go-api-producao/) e authz por recurso existem além do CSRF.
- [ ] Logout invalida a sessão server-side ([graceful shutdown](/blog/graceful-shutdown-go-producao/) 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](/blog/middleware-go-net-http-chi-gin-producao/), [autenticação e autorização](/blog/autenticacao-autorizacao-go-apis/), [OAuth/OIDC](/blog/oauth2-oidc-go-login-google-keycloak/), [CORS](/blog/cors-go-net-http-chi-gin/), [rate limiting](/blog/rate-limiting-go-api-producao/) e o tutorial de [segurança em Go](/tutoriais/go-security/). 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](/blog/projetos-go-para-portfolio-ideias-primeira-vaga/).
