Protección contra CSRF en Go
Imagina que CSRF (Cross-Site Request Forgery) es como un impostor que envía cartas falsas firmadas con tu nombre. Vamos a aprender a poner un “sello de seguridad” en cada solicitud para verificar que realmente eres tú quien la envía.
¿Qué es CSRF y por qué es peligroso?
Un ataque CSRF ocurre cuando un sitio web malicioso engaña al navegador para que realice acciones no deseadas en otro sitio donde el usuario está autenticado.
Ejemplo real:
Si estás logueado en tu banco y visitas un sitio malicioso, este podría enviar una solicitud oculta para transferir dinero sin tu consentimiento.
¿Cómo funciona la protección CSRF?
El método más común usa:
- Token CSRF: Un valor secreto único por sesión
- Cookie SameSite: Restringe cómo se envían las cookies entre sitios
- Verificación de origen: Compara el origen de la solicitud
sequenceDiagram
participant Cliente
participant Servidor
Cliente->>Servidor: GET /formulario
Servidor->>Cliente: Formulario + Token CSRF (cookie y campo hidden)
Cliente->>Servidor: POST /accion (envía token)
Servidor->>Servidor: Verifica coincidencia token-cookie
Servidor->>Cliente: 200 OK o 403 Forbidden
Implementación básica en Go
1. Middleware CSRF manual
package middleware
import (
"crypto/rand"
"encoding/base64"
"net/http"
)
// Generar token seguro
func generateToken() string {
b := make([]byte, 32)
rand.Read(b)
return base64.StdEncoding.EncodeToString(b)
}
func CSRFProtect(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Solo aplicar a métodos no-GET
if r.Method != "GET" {
// Obtener token de cookie y formulario
cookieToken, _ := r.Cookie("csrf_token")
formToken := r.FormValue("csrf_token")
// Verificar coincidencia
if cookieToken == nil || cookieToken.Value != formToken {
http.Error(w, "Token CSRF inválido", http.StatusForbidden)
return
}
}
// Para GET: establecer nueva cookie CSRF
token := generateToken()
http.SetCookie(w, &http.Cookie{
Name: "csrf_token",
Value: token,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
})
next.ServeHTTP(w, r)
})
}
2. Uso en formularios HTML
<!-- En tu template de formulario -->
<form action="/transfer" method="POST">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<input type="text" name="amount">
<button type="submit">Transferir</button>
</form>
Configuración avanzada
1. Con gorilla/csrf (recomendado)
package main
import (
"github.com/gorilla/csrf"
"github.com/gorilla/mux"
"net/http"
)
func main() {
r := mux.NewRouter()
// Configurar protección CSRF
csrfKey := []byte("32-byte-long-auth-key") // En producción usa crypto/rand
csrfMiddleware := csrf.Protect(
csrfKey,
csrf.Secure(false), // Solo para desarrollo (true en producción)
csrf.Path("/"),
csrf.FieldName("csrf_token"),
)
// Aplicar middleware
protectedRouter := csrfMiddleware(r)
// Rutas
r.HandleFunc("/form", ShowFormHandler).Methods("GET")
r.HandleFunc("/process", ProcessFormHandler).Methods("POST")
http.ListenAndServe(":8080", protectedRouter)
}
// Handler para mostrar formulario
func ShowFormHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
fmt.Fprintf(w, `
<form action="/process" method="POST">
<input type="hidden" name="csrf_token" value="%s">
<input type="text" name="data">
<button type="submit">Enviar</button>
</form>
`, csrf.Token(r))
}
2. Con APIs REST y Single Page Apps
// Middleware adaptado para APIs
func APICSRFProtect(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Excluir endpoints de API pública
if strings.HasPrefix(r.URL.Path, "/api/") {
// Verificar encabezado personalizado
if r.Header.Get("X-Requested-With") != "XMLHttpRequest" {
http.Error(w, "Cabecera CSRF requerida", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
return
}
// Método tradicional para web
cookieToken, _ := r.Cookie("csrf_token")
headerToken := r.Header.Get("X-CSRF-Token")
if r.Method != "GET" && (cookieToken == nil || cookieToken.Value != headerToken) {
http.Error(w, "Invalid CSRF token", http.StatusForbidden)
return
}
// Establecer nuevo token
token := generateToken()
w.Header().Set("X-CSRF-Token", token)
http.SetCookie(w, &http.Cookie{
Name: "csrf_token",
Value: token,
HttpOnly: true,
})
next.ServeHTTP(w, r)
})
}
Mejores prácticas de seguridad
- Tokens por sesión: Genera un nuevo token al iniciar sesión
- SameSite Cookies: Usa
SameSite=LaxoStrict - No almacenes tokens en localStorage: Usa cookies HttpOnly
- Protege todos los métodos no-idempotentes: POST, PUT, DELETE, PATCH
- Regenera tokens después de su uso en flujos críticos
Configuración recomendada para producción
csrf.Protect(
[]byte("clave-secreta-de-32-bytes"), // Generada con crypto/rand
csrf.Secure(true), // Solo HTTPS
csrf.HttpOnly(true),
csrf.SameSite(csrf.SameSiteStrictMode),
csrf.Path("/"), // Alcance de la cookie
csrf.MaxAge(3600), // 1 hora de vida
csrf.ErrorHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Solicitud CSRF rechazada", http.StatusForbidden)
})),
)
Solución de problemas comunes
1. Error: “CSRF token invalid”
- Causas:
- Cookie no se está enviando
- Token no coincide entre formulario y cookie
- Cookie configurada con dominio/path incorrecto
- Solución:
// Verifica la configuración de la cookie http.SetCookie(w, &http.Cookie{ Name: "csrf_token", Value: token, Domain: "tudominio.com", Path: "/", Secure: true, HttpOnly: true, SameSite: http.SameSiteLaxMode, })
2. Problemas con Single Page Apps
- Solución: Usa encabezados personalizados:
func APICSrfMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("X-Requested-With") == "XMLHttpRequest" { next.ServeHTTP(w, r) return } // Lógica CSRF tradicional... }) }
Pruebas de protección CSRF
1. Prueba manual con cURL
# Solicitud sin token (debería fallar)
curl -X POST http://tuservidor.com/transfer \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "amount=1000"
# Solicitud con token válido
curl -X POST http://tuservidor.com/transfer \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Cookie: csrf_token=token_valido" \
-d "amount=1000&csrf_token=token_valido"
2. Prueba automatizada con Go
func TestCSRFProtection(t *testing.T) {
ts := httptest.NewServer(CSRFMiddleware(app))
defer ts.Close()
// Test 1: Solicitud GET establece cookie
resp, _ := http.Get(ts.URL)
cookie := resp.Cookies()[0]
// Test 2: POST sin token debe fallar
resp, _ = http.Post(ts.URL, "text/plain", nil)
if resp.StatusCode != http.StatusForbidden {
t.Error("CSRF no protege solicitudes sin token")
}
// Test 3: POST con token correcto
req, _ := http.NewRequest("POST", ts.URL, nil)
req.AddCookie(cookie)
req.Form.Add("csrf_token", cookie.Value)
resp, _ = http.DefaultClient.Do(req)
if resp.StatusCode != http.StatusOK {
t.Error("CSRF bloquea solicitudes válidas")
}
}
Alternativas avanzadas
1. Doble envío de cookies
func DoubleSubmitCookie(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
cookie, _ := r.Cookie("csrf_cookie")
header := r.Header.Get("X-CSRF-Header")
if cookie == nil || cookie.Value != header {
http.Error(w, "Invalid CSRF token", http.StatusForbidden)
return
}
}
token := generateToken()
http.SetCookie(w, &http.Cookie{
Name: "csrf_cookie",
Value: token,
})
w.Header().Set("X-CSRF-Header", token)
next.ServeHTTP(w, r)
})
}
2. Encrypted Token Pattern
func EncryptedTokenMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
encryptedToken := r.FormValue("csrf_token")
// Decrypt and validate: userID + timestamp
isValid := validateEncryptedToken(encryptedToken)
if !isValid {
http.Error(w, "Invalid CSRF token", http.StatusForbidden)
return
}
}
// Generar nuevo token para la respuesta
newToken := generateEncryptedToken(currentUserID)
w.Header().Set("X-CSRF-Token", newToken)
next.ServeHTTP(w, r)
})
}
Integración con frameworks web
1. Con Echo
e := echo.New()
e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
TokenLookup: "header:X-CSRF-Token",
CookieName: "csrf",
CookiePath: "/",
}))
2. Con Gin
func CsrfMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("X-CSRF-Token")
if c.Request.Method != "GET" && token != getTokenFromSession(c) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "CSRF token invalid"})
return
}
c.Set("csrf_token", generateNewToken())
c.Next()
}
}
Conclusión
La protección CSRF es esencial para cualquier aplicación web. En Go puedes implementarla:
- Manualmente para entender los conceptos
- Usando middlewares como gorilla/csrf para producción
- Adaptando la solución a tu arquitectura (SPA, tradicional, microservicios)
Recuerda:
- Nunca confíes solo en el frontend para seguridad
- Combina CSRF con otras medidas como SameSite cookies
- Prueba regularmente tus defensas
Con esta implementación tendrás una sólida protección contra ataques CSRF en tus aplicaciones Go.