Phòng chống tấn công CSRF: Khai thác thực tế và phòng thủ với Anti-CSRF Token và Cookie SameSite

Security tutorial - IT technology blog
Security tutorial - IT technology blog

Bối cảnh & Lý do

Hãy hình dung một kỹ sư đang đăng nhập vào AWS console nội bộ hoặc cổng thanh toán. Ở một tab trình duyệt khác, họ nhấp vào một liên kết chưa qua kiểm chứng từ kênh Slack hoặc email lừa đảo (phishing). Chỉ trong vòng 200 mili-giây, email tài khoản của họ bị đổi, SSH key do kẻ tấn công kiểm soát được thêm vào, hoặc tiền bị chuyển đi. Người dùng hoàn toàn không thấy bất kỳ hộp thoại xác nhận nào.

Cross-Site Request Forgery (CSRF) khai thác cơ chế thẩm quyền ngầm định (ambient authority) trong trình duyệt web. Trình duyệt tự động đính kèm thông tin xác thực đã lưu—chẳng hạn như session cookie và header xác thực cơ bản—vào các HTTP request gửi đi. Trình duyệt làm điều này bất kể trang web nào đã khởi tạo lệnh gọi đó.

Mặc dù Chrome 80 đã áp dụng mặc định SameSite=Lax từ tháng 2 năm 2020, việc chỉ phụ thuộc vào thiết lập mặc định của trình duyệt vẫn để lại những lỗ hổng nguy hiểm. Các điều hướng GET cấp cao nhất (top-level navigation) vẫn truyền đi cookie có cờ Lax. Mobile webview, các trình duyệt nhúng cũ hơn, và những hành vi đặc thù của ITP trên Safari cũng có thể xử lý các cờ này không nhất quán. Kẻ tấn công liên tục khai thác các điểm mù này. Chẳng hạn, lỗ hổng CVE-2020-3580 trên Cisco ASA đã chứng minh CSRF có thể kích hoạt thực thi mã từ xa (RCE) ngay cả bên trong hạ tầng mạng doanh nghiệp.

Hãy xem xét một endpoint chưa được gia cố để cập nhật địa chỉ email tài khoản:

POST /api/user/email HTTP/1.1
Host: myapp.internal
Content-Type: application/x-www-form-urlencoded
Cookie: session_id=9f82a1b7c4e5d6...

[email protected]

Kẻ tấn công không bao giờ cần đánh cắp hay đọc cookie session_id. Thay vào đó, chúng buộc trình duyệt của nạn nhân thực thi request nhân danh họ. Một trang độc hại tại https://evil-site.com kích hoạt hành vi này bằng một form ẩn chỉ 5 dòng tự động submit khi tải trang:

<form id="csrfForm" action="https://myapp.internal/api/user/email" method="POST">
  <input type="hidden" name="email" value="[email protected]" />
</form>
<script>
  document.getElementById('csrfForm').submit();
</script>

Máy chủ nhận được session cookie hợp lệ cùng với request cross-origin. Nếu không có cơ chế kiểm tra thứ hai, hệ thống không thể phân biệt hành động hợp lệ của người dùng với các request giả mạo. Để ngăn chặn hình thức khai thác này, cần có hai lớp kiểm soát riêng biệt: các quy tắc ranh giới cookie tất định và xác thực token bằng mật mã học.

Cài đặt

Chúng ta sẽ xây dựng một lớp phòng thủ bằng Node.js, Express và csrf-csrf (thư viện hiện đại dựa trên HMAC thay thế cho csurf đã ngừng phát triển), cùng với cookie-parser.

Tạo một thư mục làm việc riêng và cài đặt các dependency cần thiết:

mkdir csrf-defense-lab && cd csrf-defense-lab
npm init -y
npm install express cookie-parser csrf-csrf dotenv
npm install --save-dev nodemon

Bộ công nghệ này triển khai mô hình Double Submit Cookie với cơ chế xác thực HMAC mã hóa. Nó xác minh tính xác thực của request mà không bắt buộc bạn phải lưu trữ trạng thái (state) trong Redis hay cơ sở dữ liệu quan hệ.

Cấu hình

Một hệ thống phòng thủ vững chắc kết hợp hai lớp độc lập:

  1. Cô lập cookie nghiêm ngặt (Strict Cookie Isolation): Giới hạn các trường hợp trình duyệt đính kèm session cookie trong các request cross-site.
  2. Token mật mã học cho request (Cryptographic Request Tokens): Yêu cầu một HMAC token duy nhất, không thể đoán trước trong custom header. Chính sách cùng nguồn gốc (Same-Origin Policy – SOP) ngăn chặn các origin bên thứ ba đọc hoặc giả mạo giá trị này.

1. Thiết lập khóa bí mật mật mã học

Tạo các khóa 256-bit có độ entropy cao và lưu chúng vào file .env cục bộ:

PORT=3000
NODE_ENV=development
COOKIE_SECRET=d83f12a98e54c7b20f456123489abced98471234908123490812349081234908
CSRF_SECRET=4f7b9c1d2e3a5b6c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c

2. Triển khai Middleware và bảo vệ Endpoint

Viết điểm khởi đầu của ứng dụng trong server.js:

require('dotenv').config();
const express = require('express');
const cookieParser = require('cookie-parser');
const { doubleCsrf } = require('csrf-csrf');

const app = express();
const isProduction = process.env.NODE_ENV === 'production';

app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(process.env.COOKIE_SECRET));

const {
  invalidCsrfTokenError,
  generateToken,
  doubleCsrfProtection,
} = doubleCsrf({
  getSecret: () => process.env.CSRF_SECRET,
  cookieName: isProduction ? '__Host-app.csrf' : 'app.csrf',
  cookieOptions: {
    httpOnly: true,
    sameSite: 'lax', // Dùng 'strict' cho các công cụ giao dịch có rủi ro cao
    secure: isProduction, // Bắt buộc đối với tiền tố __Host- trong môi trường production HTTPS
    path: '/',
  },
  size: 64,
  ignoredMethods: ['GET', 'HEAD', 'OPTIONS'],
  getTokenFromRequest: (req) => req.headers['x-csrf-token'] || req.body._csrf,
});

// Endpoint bắt tay (handshake): các ứng dụng single-page gọi endpoint này khi khởi tạo
app.get('/api/csrf-token', (req, res) => {
  const csrfToken = generateToken(req, res);
  res.json({ csrfToken });
});

// Giả lập đăng nhập cấp phát session cookie đã xác thực
app.post('/api/login', (req, res) => {
  res.cookie('auth_session', 'usr_sess_9a8b7c6d5e4f3a2b1c', {
    httpOnly: true,
    secure: isProduction,
    sameSite: 'lax',
    path: '/',
    maxAge: 3600000, // 1 giờ
  });
  res.json({ status: 'authenticated', userId: 42 });
});

// Route biến đổi dữ liệu (mutation) được bảo vệ chống CSRF
app.post('/api/user/email', doubleCsrfProtection, (req, res) => {
  const { email } = req.body;
  console.log(`[AUDIT] Đã cập nhật email thành công thành: ${email}`);
  res.json({ status: 'success', email });
});

// Bộ xử lý lỗi tập trung cho các token bị từ chối
app.use((err, req, res, next) => {
  if (err === invalidCsrfTokenError) {
    console.warn(`[CẢNH BÁO BẢO MẬT] Đã từ chối request giả mạo trên ${req.method} ${req.originalUrl}`);
    return res.status(403).json({
      error: 'Forbidden',
      message: 'Token CSRF không hợp lệ hoặc bị thiếu.',
    });
  }
  next(err);
});

app.listen(process.env.PORT, () => {
  console.log(`Máy chủ đang lắng nghe trên cổng ${process.env.PORT}`);
});

3. Cách các cơ chế phòng thủ phối hợp hoạt động

  • Tiền tố __Host-: Theo chuẩn RFC 6265bis, trình duyệt sẽ từ chối cookie này trừ khi nó có cờ Secure, không có thuộc tính Domain, và sử dụng Path=/. Các subdomain không thể ghi đè hay chiếm quyền cookie này.
  • SameSite=Lax: Chặn cookie khi submit form cross-site và các request AJAX chạy nền. Các thao tác nhấp link cấp cao nhất (top-level) thông thường vẫn giữ lại cookie, bảo toàn trải nghiệm người dùng khi truy cập từ công cụ tìm kiếm.
  • Custom Header x-csrf-token: Các ứng dụng Single Page (React, Vue, Angular) lấy token khi khởi động và đính kèm vào các lệnh gọi API. Kẻ tấn công cross-origin không thể đọc được token này do Chính sách cùng nguồn gốc (Same-Origin Policy) của trình duyệt cấm đọc response giữa các origin khác nhau.

Kiểm thử & Giám sát

Kiểm thử cả luồng client hợp lệ lẫn các nỗ lực giả mạo độc hại bằng curl.

1. Kiểm thử luồng người dùng hợp lệ

Xác thực, lưu session cookie, lấy token và thực hiện cập nhật được ủy quyền:

# 1. Đăng nhập và lưu session cookie vào cookie jar cục bộ
curl -s -c cookies.txt -X POST http://localhost:3000/api/login

# 2. Lấy CSRF token đồng thời gửi kèm cookie jar
CSRF_TOKEN=$(curl -s -b cookies.txt -c cookies.txt http://localhost:3000/api/csrf-token | jq -r .csrfToken)
echo "Token nhận được: $CSRF_TOKEN"

# 3. Thực hiện thao tác cập nhật đã được ủy quyền
curl -s -b cookies.txt \
  -H "Content-Type: application/json" \
  -H "x-csrf-token: $CSRF_TOKEN" \
  -d '{"email":"[email protected]"}' \
  http://localhost:3000/api/user/email

Máy chủ trả về HTTP 200 OK với {"status":"success","email":"[email protected]"}.

2. Mô phỏng tấn công CSRF

Gửi request đi kèm session cookie của nạn nhân, nhưng bỏ qua custom header:

curl -i -b cookies.txt \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]"}' \
  http://localhost:3000/api/user/email

Middleware từ chối request ngay lập tức:

HTTP/1.1 403 Forbidden
Content-Type: application/json; charset=utf-8

{
  "error": "Forbidden",
  "message": "Token CSRF không hợp lệ hoặc bị thiếu."
}

3. Giám sát và cảnh báo trong môi trường Production

Các lỗi 403 lặp lại liên tục trên những endpoint thay đổi dữ liệu là dấu hiệu của hành vi brute-force hoặc bản build phía client bị lỗi. Hãy xem xét chúng như các sự kiện an ninh.

Đẩy log lỗi JSON có cấu trúc về các công cụ thu thập log như Vector, Fluentbit hoặc Datadog:

if (err === invalidCsrfTokenError) {
  const securityLog = {
    timestamp: new Date().toISOString(),
    event: 'CSRF_VALIDATION_FAILURE',
    client_ip: req.headers['x-forwarded-for'] || req.socket.remoteAddress,
    origin: req.headers['origin'] || null,
    referer: req.headers['referer'] || null,
    path: req.originalUrl,
    method: req.method,
    user_agent: req.headers['user-agent'],
  };
  console.error(JSON.stringify(securityLog));
}

Hãy thiết lập ngưỡng cảnh báo trong công cụ giám sát log cho các đột biến vượt quá 10 sự kiện thất bại trong vòng 60 giây từ một IP duy nhất, hoặc bất kỳ request nào có header Origin không khớp với danh sách domain được phép. Phát hiện sớm các dấu hiệu này giúp bảo vệ an toàn cho tài khoản người dùng trước khi cuộc tấn công khai thác thành công.

Share: