CSRF token — đã đến lúc vứt đi?
Datasette vừa thay toàn bộ CSRF token bằng một dòng header trình duyệt. Câu chuyện về bảo mật web đang thay đổi ngay dưới tay bạn.
Bụi WireCái đêm mình xóa 47 dòng hidden input
Mình nhớ rõ cái đêm đó. 11 giờ, ngồi grep toàn bộ codebase tìm chỗ nào thiếu csrftoken() trong form. Một cái form nhỏ xíu — nút "Delete comment" — bị miss, và QA vừa file bug "CSRF validation failed" lần thứ ba trong sprint. Cảm giác như đang kiểm tra vé giấy từng hành khách trên chuyến xe buýt, trong khi hệ thống thẻ từ đã nằm sẵn trên xe từ lâu.
Tuần trước, Simon Willison — tác giả Datasette — vừa push một PR làm mình muốn đứng dậy vỗ tay: xóa sạch toàn bộ CSRF token, thay bằng kiểm tra header Sec-Fetch-Site của trình duyệt. Một thay đổi tưởng nhỏ mà lật lại cách nghĩ về bảo mật web.
Trước: vé giấy cho mỗi chuyến đi
Cơ chế CSRF token truyền thống hoạt động thế này: mỗi form HTML phải nhét thêm một input ẩn chứa token ngẫu nhiên. Server sinh token, gửi xuống client, client gửi lại kèm request — server kiểm tra khớp thì mới xử lý.
Hình dung thế này: bạn có một ứng dụng nội bộ với 20 cái form — từ login, tạo project, đến xóa record. Mỗi form đều cần một dòng hidden input chứa token. Giả sử team bạn 4 người, mỗi người viết vài template. Ai quên dòng đó? Ai nhớ disable CSRF cho API endpoint gọi từ mobile app? Ai viết test cover trường hợp token hết hạn?
Datasette dùng thư viện asgi-csrf để xử lý chuyện này. Nhưng Simon thừa nhận thẳng: nó là "something of a pain to work with". Mình tin bất kỳ dev nào từng maintain một web app đủ lớn đều gật đầu đồng cảm.
Sau: trạm thu phí tự động
Sec-Fetch-Site là một fetch metadata header mà trình duyệt hiện đại tự động gắn vào mọi request. Nó cho server biết: request này đến từ cùng origin (same-origin), cross-site (cross-site), hay do user gõ URL trực tiếp (none).
Dịch sang tiếng người: thay vì bắt mỗi form tự mang theo vé, giờ trạm thu phí đọc biển số xe tự động. Xe nào từ đúng làn vào thì cho qua, xe lạ thì chặn — hành khách không cần làm gì thêm.
Cách Datasette triển khai:
- Request có
Sec-Fetch-Site: same-origin→ cho qua - Request không có header này (CLI tool, curl, API client) → cũng cho qua, vì không phải trình duyệt thì không có CSRF risk
- Request có
Sec-Fetch-Site: cross-site→ chặn
Kết quả? Xóa toàn bộ hidden input trong template. Xóa luôn plugin hook skip_csrf. Code gọn hơn, ít chỗ để bug ẩn nấp.
Cách tiếp cận này không phải Datasette nghĩ ra. Nó đến từ nghiên cứu của Filippo Valsorda và đã ship chính thức trong Go 1.25 từ tháng 8/2025. Giờ Datasette mang nó sang thế giới Python/ASGI. Claude Code thực hiện phần lớn code (10 commits), với Simon hướng dẫn sát sao và GPT-5.4 cross-review.
Ba cái hố trước khi bạn lao vào xóa token
Đừng vội mở PR xóa CSRF token sáng mai. Mình liệt kê vài bẫy để bạn khỏi giẫm:
Trình duyệt cũ không gửi header này. Nếu user base của bạn còn dính trình duyệt từ thời "IE còn thở", bạn cần fallback. Datasette chọn cách: không có header thì cho qua — giả định đó là non-browser client. Nhưng nếu app bạn 100% browser-facing, đây là lỗ hổng cần cân nhắc kỹ.
API dual-use — vừa browser vừa programmatic. Ví dụ cụ thể: team mình từng có endpoint /api/upload vừa gọi từ form dashboard, vừa gọi từ script Python nội bộ. Với CSRF token, script phải fetch token trước — phiền nhưng tường minh. Với Sec-Fetch-Site, script Python không gửi header → cho qua luôn. Tiện hơn, nhưng authentication layer phải đủ chắc để bù lại.
Đừng nhầm "bỏ CSRF token" với "bỏ bảo mật." Sec-Fetch-Site chỉ thay cơ chế chống CSRF. Auth, rate limiting, input validation — vẫn phải đầy đủ, không thiếu món nào.
Thử ngay chiều nay — 30 phút là đủ
Bạn không cần migrate cả project. Bốn bước để hiểu bức tranh thực tế trên chính app của mình:
Bước 1: Mở DevTools trên Chrome hoặc Firefox, vào tab Network, click bất kỳ request nào. Tìm header Sec-Fetch-Site. Bạn sẽ thấy same-origin cho request nội bộ — nó đã ở đó từ lâu, chỉ là chưa ai dùng.
Bước 2: Viết một middleware nhỏ chỉ để log giá trị header này cho mọi POST request. Chạy song song với CSRF token hiện tại, không thay thế gì cả.
# Ví dụ middleware cho FastAPI
@app.middleware("http")
async def log_sec_fetch(request, call_next):
if request.method == "POST":
sec_fetch = request.headers.get("sec-fetch-site", "missing")
print(f"POST {request.url.path} -> Sec-Fetch-Site: {sec_fetch}")
return await call_next(request)
Bước 3: Chạy vài ngày, đọc log. Bao nhiêu request có header? Có request nào cross-site bất thường không? Dữ liệu thực tế từ app của bạn sẽ trả lời câu hỏi "có nên migrate chưa" tốt hơn bất kỳ bài blog nào.
Bước 4: Nếu muốn đi sâu, đọc PR #2689 của Datasette — Simon viết description bằng tay rất kỹ, kèm upgrade guide. Đây là open-source, bạn đọc được từng dòng diff.
Khi agent gọi API — CSRF có còn ý nghĩa?
Một góc nhìn đáng suy nghĩ: thế giới AI agent đang bùng nổ, ngày càng nhiều request đến server không phải từ trình duyệt. TinyFish vừa ra mắt nền tảng hạ tầng web cho agent — gom search, fetch, browser automation vào một API key duy nhất. Khi agent dùng headless Chromium để điền form trên web, nó gửi Sec-Fetch-Site như trình duyệt thật. Nhưng khi agent gọi API trực tiếp qua HTTP client thì không.
Hiểu nôm na: thế giới web đang tách thành hai làn rõ rệt — browser traffic và programmatic traffic. Sec-Fetch-Site giúp server nhận diện hai làn đó một cách tự nhiên, thay vì ép cả hai mua cùng loại vé giấy.
Một dòng đúc kết
CSRF token đã phục vụ tốt suốt hơn một thập kỷ. Nhưng trình duyệt giờ tự biết nói "tôi đến từ đâu" — và thành thật mà nói, bạn đã tin trình duyệt từ lâu rồi, chỉ là chưa để nó chứng minh. Bảo mật tốt nhất không phải loại phức tạp nhất — mà là loại mà dev không thể quên implement.
---
Bụi Wire — nghiện đọc release notes lúc 2 giờ sáng
Nguồn tham khảo
- datasette PR #2689: Replace token-based CSRF with Sec-Fetch-Site header protection
- Google AI Research Proposes Vantage: An LLM-Based Protocol for Measuring Collaboration, Creativity, and Critical Thinking - MarkTechPost
- TinyFish AI Releases Full Web Infrastructure Platform for AI Agents: Search, Fetch, Browser, and Agent Under One API Key - MarkTechPost