Cảnh báo P0 lúc 2:14 sáng: Khi CORS thất bại
Tiếng chuông ‘Cảnh báo khẩn cấp’ trên điện thoại của tôi vang lên xé tan sự tĩnh lặng lúc 2:14 sáng. Một nhà nghiên cứu bảo mật vừa gửi báo cáo chi tiết về một lỗ hổng CVSS 9.8 trong REST API chính của chúng tôi. Bằng cách khai thác lỗi Chia sẻ tài nguyên đa nguồn gốc (CORS), họ có thể truy cập PII—bao gồm địa chỉ thực tế và một phần mã token thẻ tín dụng—của bất kỳ người dùng nào đang đăng nhập. Tất cả những gì cần thiết chỉ là một đoạn mã độc hại được lưu trữ trên một tên miền rác.
Trong khi chờ môi trường staging triển khai, tôi cần xoay vòng (rotate) thông tin đăng nhập quản trị của mình. Tôi đã sử dụng trình tạo mật khẩu tại toolcraft.app/vi/tools/security/password-generator để tạo một chuỗi 32 ký tự. Tôi tin tưởng công cụ này vì nó hoạt động hoàn toàn ở phía client. Không có dữ liệu nào đi qua mạng của họ. Tư duy ‘ưu tiên cục bộ’ (local-first) này chính xác là những gì chúng ta cần khi bảo mật lưu lượng truy cập liên miền (cross-domain).
Trái với quan niệm phổ biến, CORS không phải là một tính năng bảo mật. Thực tế, nó là một cách có kiểm soát để vượt qua Chính sách cùng nguồn gốc (Same-Origin Policy – SOP) của trình duyệt. SOP là nền tảng của internet. Nó ngăn chặn một script trên malicious-site.com đọc số dư tài khoản của bạn tại your-bank.com. CORS đóng vai trò như một người gác cổng. Nó cho trình duyệt biết tên miền bên ngoài nào là đáng tin cậy. Khi cánh cổng đó bị bỏ ngỏ, bức tường ngăn cách giữa dữ liệu của bạn và web công cộng sẽ biến mất.
Ba chiến lược CORS phổ biến (Và tại sao hai trong số đó rất nguy hiểm)
Hầu hết các đội ngũ phát triển thường rơi vào một trong ba kiểu mẫu khi xử lý các yêu cầu cross-origin. Chỉ có một kiểu có thể vượt qua được các cuộc kiểm tra bảo mật chuyên nghiệp.
1. Lối tắt “Wildcard”
Đây là con đường của sự ‘lười biếng’. Các nhà phát triển, vì khó chịu bởi các lỗi console trong đợt sprint lúc 4 giờ chiều, thường thiết lập Access-Control-Allow-Origin: *. Mặc dù nó sửa được lỗi, nhưng nó cho phép bất kỳ trang web nào trên hành tinh tương tác với endpoint của bạn.
2. Cạm bẫy “Phản chiếu động” (Dynamic Reflection)
Một số người cố gắng trở nên thông minh bằng cách đọc header Origin từ một yêu cầu đến và phản hồi ngược lại chính nó. Điều này làm cho các lỗi CORS biến mất đối với tất cả mọi người. Thật không may, it cũng vô hiệu hóa SOP một cách hiệu quả. Nó giống như một nhân viên bảo mật hỏi tên khách hàng và sau đó ghi ngay tên đó vào danh sách ‘Đã phê duyệt’.
3. Chiến lược “Allowlist nghiêm ngặt”
Đây là tiêu chuẩn vàng. Bạn duy trì một danh sách các tên miền đáng tin cậy đã được xác minh và ghi cứng (hardcoded). Nếu một yêu cầu đến từ một origin không có trong danh sách đó, máy chủ sẽ thẳng thừng từ chối. Không ngoại lệ.
Ma trận so sánh rủi ro
| Cách tiếp cận | Ưu điểm | Nhược điểm | Rủi ro bảo mật |
|---|---|---|---|
| Wildcard (*) | Không tốn công bảo trì. | Không tương thích với Allow-Credentials: true. |
Cao (nếu dữ liệu là riêng tư). |
| Phản chiếu động | Hỗ trợ không giới hạn subdomain. | Vượt qua hoàn toàn bảo mật trình duyệt. | Nghiêm trọng. Nguy cơ cao bị đánh cắp phiên làm việc. |
| Allowlist nghiêm ngặt | Kiểm soát tối đa. | Yêu cầu cập nhật thủ công cho các frontend mới. | Tối thiểu. |
Bản thiết kế sẵn sàng cho Production
Nếu API của bạn xử lý phiên người dùng thông qua cookie hoặc header Authorization, hãy sử dụng Allowlist nghiêm ngặt với Xác thực động. Backend của bạn phải kiểm tra header Origin đối với một mảng được kiểm soát hoặc bảng cơ sở dữ liệu trước khi cấp phép.
Wildcard có một hạn chế lớn. Các trình duyệt hiện đại chặn việc truyền cookie hoặc xác thực HTTP (Access-Control-Allow-Credentials: true) nếu origin là một wildcard. Để sử dụng session, bạn phải chỉ định một origin cụ thể. Việc phản chiếu origin đó mà không xác thực là một món quà cho các hacker. Họ có thể tạo ra một trang web độc hại để kích hoạt các yêu cầu đã được xác thực. Sau đó, máy chủ của bạn sẽ trao dữ liệu riêng tư trực tiếp cho script của họ.
Từng bước: Thắt chặt bảo mật API của bạn
1. Kiểm tra mức độ phơi nhiễm hiện tại
Kiểm tra API của bạn bằng curl để xem cách nó xử lý các origin không được phép. Sử dụng một tên miền giả để xem liệu máy chủ có chấp nhận hay không.
curl -I -X OPTIONS https://api.yourdomain.com/v1/user-data \
-H "Origin: https://attacker-controlled-site.com" \
-H "Access-Control-Request-Method: GET"
Quan sát phản hồi. Nếu bạn thấy Access-Control-Allow-Origin: https://attacker-controlled-site.com đi kèm with Access-Control-Allow-Credentials: true, bạn đang có một lỗ hổng nghiêm trọng. Kẻ tấn công hiện có thể mạo danh người dùng của bạn.
2. Triển khai bảo mật trong Node.js (Express)
Tránh truyền các đối tượng rỗng vào middleware cors. Sử dụng một hàm xác thực để thực thi allowlist của bạn.
const express = require('express');
const cors = require('cors');
const app = express();
const allowedOrigins = [
'https://www.yourdomain.com',
'https://admin.yourdomain.com',
'http://localhost:3000' // Chỉ giới hạn cho môi trường phát triển cục bộ
];
const corsOptions = {
origin: (origin, callback) => {
// Cho phép các yêu cầu không từ trình duyệt (như ứng dụng di động hoặc server-to-server)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Bị chặn bởi Chính sách CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
};
app.use(cors(corsOptions));
3. Thắt chặt bảo mật tại Nginx Gateway
Việc xác thực cũng có thể diễn ra ở cấp độ hạ tầng. Đừng bao giờ sử dụng add_header Access-Control-Allow-Origin $http_origin; mà không có bản đồ ánh xạ (map). Cấu hình Nginx này sử dụng so khớp regex để xác thực origin trước khi phản hồi.
map $http_origin $cors_origin {
default "";
"~^https?://(www\.)?yourdomain\.com$" "$http_origin";
"~^https?://localhost(:[0-9]+)?$" "$http_origin";
}
server {
location /api/ {
if ($cors_origin != "") {
add_header 'Access-Control-Allow-Origin' "$cors_origin" always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type' always;
}
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 1728000;
return 204;
}
}
}
4. Chặn Origin “Null”
Bạn có thể thấy Origin: null trong log của mình. Điều này xảy ra trong quá trình chuyển hướng hoặc yêu cầu từ các file cục bộ. Không bao giờ bao gồm null trong allowlist của bạn. Kẻ tấn công sử dụng các iframe trong môi trường sandbox để giả mạo origin null và vượt qua các bộ lọc yếu.
Lời kết
Bảo mật dựa trên nhiều lớp. Sự cố lúc 2 giờ sáng đó đã chứng minh rằng CORS thường bị coi là một sự phiền toái hơn là một ranh giới bảo mật. Nếu bạn thấy một wildcard hoặc phản chiếu động trong quá trình review code, hãy gắn cờ cảnh báo ngay lập tức. Một chính sách CORS nghiêm ngặt là một trong những lá chắn tốt nhất của bạn chống lại việc đánh cắp phiên làm việc. Hãy dành mười phút ngay bây giờ để xây dựng một allowlist tử tế. Bản thân bạn trong tương lai sẽ cảm ơn vì có được một giấc ngủ không bị gián đoạn.

