CORS và những hiểu lầm tai hại trong an toàn ứng dụng web
Giải mã cơ chế Cross-Origin Resource Sharing, preflight OPTIONS và các cấu hình sai lầm nghiêm trọng khiến rò rỉ dữ liệu người dùng.
TL;DR: Rất nhiều kỹ sư phần mềm nhầm tưởng CORS là một tính năng bảo mật hay bức tường lửa của server để ngăn chặn kẻ tấn công gọi API. Trên thực tế, CORS sinh ra nhằm mục đích nới lỏng Same-Origin Policy, chỉ thị cho trình duyệt cho phép code JavaScript đọc được response từ một domain khác. Sự ngộ nhận này thường dẫn đến các cấu hình sai lầm nghiêm trọng, như tự động phản hồi lại header Access-Control-Allow-Origin từ request kèm theo Access-Control-Allow-Credentials: true, hoặc viết regex kiểm tra subdomain hời hợt, biến toàn bộ dữ liệu nội bộ của người dùng thành mục tiêu khai thác của các trang web độc hại.
Khi một lập trình viên frontend gọi API từ ứng dụng React chạy tại http://localhost:3000 tới backend Spring Boot tại https://api.mycorp.com, bảng điều khiển DevTools lập tức nổ lỗi màu đỏ: "CORS policy: No 'Access-Control-Allow-Origin' header is present". Phản xạ quen tay của nhiều người là lên mạng tìm kiếm và sao chép đoạn code cấu hình: res.setHeader("Access-Control-Allow-Origin", "*"). Ứng dụng chạy mượt mà, nhưng họ không nhận ra mình vừa mở toang cánh cửa để bất kỳ website độc hại nào trên internet cũng có thể đọc trộm dữ liệu API của người dùng.
1. Bản chất của CORS là nới lỏng hay thắt chặt an ninh?
Cross-Origin Resource Sharing (CORS) là một tiêu chuẩn của W3C cho phép máy chủ tuyên bố rằng: "Tôi tin tưởng domain X, trình duyệt hãy cho phép JavaScript của domain X đọc dữ liệu của tôi".
- SOP là mặc định: Mọi hành vi đọc chéo domain đều bị chặn đứng.
- CORS là ngoại lệ: Server gửi kèm các HTTP response headers đặc thù để hướng dẫn trình duyệt mở cửa cho client.
Điều tối quan trọng mà mọi kỹ sư cần ghi nhớ: CORS là cơ chế bảo vệ Client, không bảo vệ Server. Nếu một hacker viết script Python hoặc dùng cURL, họ không hề bị ràng buộc bởi CORS và vẫn gọi API của bạn bình thường. Việc cấu hình CORS sai lầm chỉ khiến trình duyệt của người dùng hợp lệ bị lợi dụng để đánh cắp dữ liệu.

2. Request đơn giản và Preflight OPTIONS hoạt động ra sao?
Trình duyệt chia các yêu cầu cross-origin thành hai nhóm riêng biệt:
Nhóm 1: Yêu cầu đơn giản (Simple Requests)
Một request được coi là đơn giản khi thỏa mãn đồng thời:
- Sử dụng phương thức:
GET,POST, hoặcHEAD. - Chỉ chứa các headers an toàn cơ bản:
Accept,Accept-Language,Content-Language, vàContent-Typethuộc ba loại (text/plain,multipart/form-data,application/x-www-form-urlencoded).
Với nhóm này, trình duyệt gửi request thẳng tới server kèm header Origin: https://frontend.com. Server xử lý xong và trả về kết quả kèm Access-Control-Allow-Origin. Nếu giá trị này khớp, JavaScript được đọc response; nếu không khớp, trình duyệt giấu response đi.
Nhóm 2: Yêu cầu cần thẩm định trước (Preflight Requests)
Nếu request gửi dữ liệu dạng application/json, sử dụng phương thức PUT/DELETE, hoặc đính kèm custom header như Authorization:
- Trình duyệt tạm hoãn gửi request thật.
- Trình duyệt tự động phát một HTTP request thăm dò với phương thức
OPTIONS:Access-Control-Request-Method: PUTAccess-Control-Request-Headers: authorization,content-type
- Server nhận OPTIONS và phản hồi cho phép:
Access-Control-Allow-Origin: https://frontend.comAccess-Control-Allow-Methods: GET, POST, PUTAccess-Control-Allow-Headers: authorization, content-type
- Chỉ khi preflight thành công, trình duyệt mới gửi request
PUTthật.

Hình 2.2b: Simple Request được gửi thẳng tới server, trong khi Preflight Request bắt buộc phải vượt qua bước bắt tay OPTIONS trước khi truyền dữ liệu thật.
Dưới đây là một bài tập dự đoán phản xạ nhanh về cơ chế preflight:
Một ứng dụng gọi lệnh fetch() gửi request POST với header 'Content-Type: application/json' tới domain khác. Trình duyệt sẽ gửi bao nhiêu HTTP request qua mạng?
3. Cấu hình Origin wildcard đi kèm Credentials vỡ vụn thế nào?
Nhiều API muốn nhận cookie phiên đăng nhập nên cấu hình:
Access-Control-Allow-Credentials: true
Nhưng vì lười liệt kê domain, lập trình viên đặt thêm:
Access-Control-Allow-Origin: *
Phản ứng của Trình duyệt:
Chuẩn bảo mật của trình duyệt nghiêm cấm tuyệt đối tổ hợp này! Nếu server trả về wildcard * cùng với credentials true, trình duyệt sẽ chặn đứng hoàn toàn và báo lỗi bảo mật ngay tại console.
Sai lầm tai hại khi "lách luật":
Để qua mặt lỗi trên của trình duyệt, nhiều lập trình viên viết code động: lấy giá trị header Origin mà client gửi lên và gán thẳng lại vào Access-Control-Allow-Origin:
// Insecure Express.js dynamic origin reflection
app.use((req, res, next) => {
// Reflect any incoming origin and grant credentials
res.header("Access-Control-Allow-Origin", req.headers.origin);
res.header("Access-Control-Allow-Credentials", "true");
next();
});
Hậu quả: Kẻ tấn công tạo trang web https://attacker.com, mở trình duyệt của nạn nhân và gửi request tới API. Header Origin: https://attacker.com được server phản hồi chấp thuận, kèm quyền đọc cookies. Kẻ tấn công rút cạn toàn bộ dữ liệu bí mật của tài khoản!
Thử ngẫmTại sao các trình duyệt hiện đại không cho phép code JavaScript tự sửa hoặc giả mạo trường header Origin khi gửi request?
4. Những bẫy Regex kiểm tra Origin phổ biến bị bypass ra sao?
Để tránh dùng wildcard, các kỹ sư thường viết biểu thức chính quy (Regex) để kiểm tra danh sách domain được phép. Đây là nơi hàng loạt lỗi bảo mật tinh vi xuất hiện:
Bẫy 1: Thiếu ký tự neo kết thúc ranh giới ($)
// Intended to match example.com and subdomains
if (/^https:\/\/.*example\.com/.test(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
}
Kẻ tấn công đăng ký domain: https://example.com.attacker.com hoặc https://example.com-evil.org.
Chuỗi này hoàn toàn khớp với regex trên vì regex không có ký tự neo kết thúc $!
Bẫy 2: Quên escape dấu chấm (.)
Trong Regex, dấu chấm . đại diện cho bất kỳ ký tự nào.
Nếu viết: /https:\/\/my-api.com$/
Kẻ tấn công có thể đăng ký: https://my-apixcom (ký tự x thay thế dấu chấm) và vượt qua bộ lọc dễ dàng.
Bẫy 3: Tin tưởng Origin null
Khi mở một file HTML cục bộ từ ổ cứng hoặc một trang chạy trong sandbox iframe:
Origin: null
Nhiều backend cho phép giá trị null trong whitelist, tạo điều kiện cho kẻ tấn công nhúng mã độc vào thẻ <iframe sandbox="allow-scripts"> để gửi request với Origin là null và vượt qua kiểm tra.
// CORS filter validating domain pattern
const allowedPattern = /^https://.*.mycompany.com/;
function checkCors(origin) {
// Security bug: missing end anchor $
return allowedPattern.test(origin);
}5. Cấu hình CORS chuẩn kiến trúc an toàn cho API như thế nào?
Một cấu hình CORS an toàn theo tiêu chuẩn Secure-by-Design phải tuân thủ 3 nguyên tắc:
- Danh sách trắng tĩnh (Static Whitelist): Lưu danh sách các origin được phép trong một tập hợp cố định (Set/Array) thay vì dùng regex phức tạp:
TypeScript
const ALLOWED_ORIGINS = new Set([ "https://mycompany.com", "https://admin.mycompany.com", "https://staging.mycompany.com", ]); function getCorsOrigin(requestOrigin?: string): string | null { if (requestOrigin && ALLOWED_ORIGINS.has(requestOrigin)) { return requestOrigin; } return null; // Return null when origin is not allowed } - Khai báo header Vary: Luôn thêm header
Vary: Originvào mọi response. Header này thông báo cho các bộ nhớ đệm (Web Cache, CDN) biết rằng response được sinh ra tùy theo origin của từng client, tránh tình trạng cache nhầm response CORS của người khác. - Giới hạn thời gian Cache Preflight: Thiết lập
Access-Control-Max-Age: 600(10 phút) để trình duyệt không phải liên tục gửi requestOPTIONScho mỗi lần gọi API, vừa giảm tải cho server vừa duy trì sự an toàn.
6. Tổng kết & Bảng kiểm tra an toàn cấu hình CORS
CORS là một hợp đồng nghiêm ngặt giữa trình duyệt và máy chủ:
- Đừng bao giờ coi CORS là công cụ bảo vệ backend; luôn kiểm tra xác thực quyền hạn trên từng endpoint.
- Tuyệt đối không tự động phản chiếu lại trường
Origincủa client khi bậtCredentials: true. - Sử dụng so khớp danh sách trắng chính xác (Exact string matching) thay cho các biểu thức chính quy dễ sơ hở.
[!TIP] Nếu dự án backend của bạn sử dụng Java / Spring Boot, hãy xem cách cấu hình
CorsConfigurationSourcebảo mật và chi tiết tại bài học CORS trong Spring Security.
7. Tự kiểm tra (Self-check)
Hãy củng cố kiến thức giải mã cấu hình CORS của bạn qua các câu hỏi truy xuất chủ động dưới đây:
- Q1Vì sao cấu hình Access-Control-Allow-Origin: * đi kèm Access-Control-Allow-Credentials: true lại bị trình duyệt từ chối?
- Q2Khi nào trình duyệt bắt buộc phải gửi một request Preflight OPTIONS trước khi gửi request thật?
- Q3Một biểu thức chính quy CORS có dạng /^https:\/\/.*\.corp\.com/ gặp phải lỗ hổng gì?
- Q4Tại sao cần gửi kèm header Vary: Origin khi cấu hình CORS trên máy chủ có sử dụng CDN?
- Q5CORS có bảo vệ được hệ thống nếu kẻ tấn công viết script Python để tấn công API không? Vì sao?
- Q6Origin: null phát sinh trong những trường hợp nào và tại sao không nên đưa vào danh sách tin cậy?
Bài tiếp theo: Cookie Security Flags: HttpOnly, Secure & SameSite
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Same-Origin Policy: Trụ cột bảo mật trình duyệt web
Bài tiếpCookie Security Flags: HttpOnly, Secure và SameSite
Thảo luận & góp ý
Hỏi đáp về bài này