惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Martin Fowler
Martin Fowler
WordPress大学
WordPress大学
月光博客
月光博客
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
大猫的无限游戏
大猫的无限游戏
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园 - 聂微东
Apple Machine Learning Research
Apple Machine Learning Research
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
雷峰网
雷峰网
小众软件
小众软件
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 叶小钗
美团技术团队
宝玉的分享
宝玉的分享
Hugging Face - Blog
Hugging Face - Blog
阮一峰的网络日志
阮一峰的网络日志
A
About on SuperTechFans
Jina AI
Jina AI
D
Docker
Last Week in AI
Last Week in AI
MongoDB | Blog
MongoDB | Blog
Stack Overflow Blog
Stack Overflow Blog
Microsoft Azure Blog
Microsoft Azure Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
CSRF: Why Double-Submit Cookie Falls Short for Financial-...
Odilon HUGONNOT · 2026-06-17 · via DEV Community

Odilon HUGONNOT

In the previous articles, we hardened the login: constant timing, progressive lockout, no existence leaks. The user is authenticated. Now the question is: how do you protect the actions they take once logged in?

CSRF. The topic everyone thinks they've mastered because they put a token in a hidden field in 2015. Except the double-submit cookie — the pattern found in 80% of implementations — has weaknesses most developers ignore.

Double-submit cookie: the pattern and its limits

Double-submit cookie works like this: the server sets a cookie csrf_token=abc123, the form sends the same token in a hidden field, the server compares both. If an attacker can't read the domain's cookies, they can't forge the request.

The problem: the attacker doesn't need to read the cookie. They need to set it. And in several scenarios, that's possible:

  • Compromised subdomain — an XSS on blog.example.com can set a cookie for .example.com
  • HTTP downgrade — if a single page on the domain is accessible via HTTP (even a redirect), a MITM attacker can inject a Set-Cookie
  • Cookie tossing — some browsers accept cookies set by parent domains

If the attacker can set the CSRF cookie, they control both values (cookie and form field) and double-submit protects nothing.

For an authentication service handling sensitive operations, that's not acceptable. OWASP ASVS V4.2.2 explicitly recommends the synchronizer token pattern for high-risk contexts.

Synchronizer token: server-side storage

The synchronizer token is straightforward: the CSRF token is generated server-side, stored in the session (DB or Redis), and compared in constant-time on every mutative request.

// Token generation at session creation
func generateCSRFToken() string {
    b := make([]byte, 32)
    if _, err := rand.Read(b); err != nil {
        panic(err) // crypto/rand failure = immediate stop
    }
    return base64.RawURLEncoding.EncodeToString(b)
}

// Store in session
session.CSRFToken = generateCSRFToken()
store.Save(session)

Comparison uses subtle.ConstantTimeCompare — not == — to avoid a timing oracle on the token:

func validateCSRF(session *Session, provided string) bool {
    expected := []byte(session.CSRFToken)
    got := []byte(provided)
    return subtle.ConstantTimeCompare(expected, got) == 1
}

The advantage: the token is never in a cookie. The attacker can neither read it nor set it. The only way to obtain it is DOM access (and at that point, it's XSS, not CSRF).

Middleware wire-order

The order in which you chain your middlewares isn't cosmetic. For CSRF, the middleware must execute:

  1. After session-load — otherwise you don't have the token to compare
  2. Before refresh-user — otherwise a forged CSRF request could trigger a refresh with side-effects
// Correct order
mux.Use(sessionMiddleware)     // 1. load session
mux.Use(csrfMiddleware)        // 2. validate CSRF token
mux.Use(refreshUserMiddleware) // 3. refresh user data
mux.Use(handler)               // 4. process request

I saw a codebase where CSRF middleware came after refresh-user. Result: a forged CSRF request triggered a SELECT + UPDATE last_seen on the user before being rejected. Not a vulnerability per se, but an unnecessary side-effect and a signal that wire-order wasn't thought through.

X-CSRF-Token header for JS requests

HTML forms send the token in a hidden field. But fetch() or XMLHttpRequest calls don't go through a form. For those, the standard pattern: custom X-CSRF-Token header.

// Client side: read token from a meta tag
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;

fetch('/api/action', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': csrfToken,
    },
    body: JSON.stringify(data),
});

// Server side: accept token from header OR form field
func extractCSRFToken(r *http.Request) string {
    // Header takes priority (JS requests)
    if token := r.Header.Get("X-CSRF-Token"); token != "" {
        return token
    }
    // Fallback to form field (HTML forms)
    return r.FormValue("csrf_token")
}

The custom header has a bonus: cross-origin requests can't add custom headers without a CORS preflight. That's an extra protection layer, though it shouldn't be the only one.

Token rotation

Two schools: one token per session, or one per request. My take: one per session is sufficient for the vast majority of cases. Per-request tokens add complexity (race conditions on parallel requests, back button invalidation) for marginal security gain.

The only reason to rotate the token: after a privilege change (login, role escalation). And at that point, you're regenerating the entire session anyway.

Conclusion

Double-submit cookie is a fine default for standard applications. But when you handle authentication, financial operations, or high-risk actions, the server-side synchronizer token is the right choice. The token is never in a cookie, comparison is constant-time, and middleware wire-order enforces discipline on the processing chain.

So far we've covered login, brute force, and CSRF. All on a service running mTLS. As it happens, mTLS has its own subtleties: how do you serve three different audiences on a single TLS port with distinct client auth levels? That's the subject of the next article.