CORS - Hiểu đúng để không còn 'sợ'
Nếu bạn là web developer, chắc hẳn đã không ít lần gặp lỗi CORS trong console. Nhưng thực sự CORS là gì? Tại sao nó tồn tại? Và quan trọng nhất - làm sao để xử lý nó đúng cách?


Bối cảnh: Vấn đề bảo mật web
Để hiểu CORS, trước tiên cần hiểu vấn đề mà nó giải quyết.
Tấn công CSRF - Mối đe dọa thực tế
Giả sử bạn đang đăng nhập vào bank.com
:
- Sau khi login, browser lưu session cookie
- Mọi request tiếp theo đến
bank.com
đều tự động gửi kèm cookie này - Server
bank.com
dựa vào cookie để xác thực người dùng
Vấn đề phát sinh khi:
- Bạn vào một trang web độc hại
evil.com
- Trang này chứa code JavaScript gửi request đến
bank.com/transfer
- Browser tự động gửi kèm session cookie (vì đây là request đến
bank.com
) - Server
bank.com
nhận request kèm cookie hợp lệ → Thực hiện chuyển tiền
Đây là lỗ hổng CSRF (Cross-Site Request Forgery) - kẻ tấn công lợi dụng session của người dùng để thực hiện các hành động không mong muốn.
Same-Origin Policy (SOP)
Để ngăn chặn CSRF, các browser triển khai Same-Origin Policy:
Origin là gì?
Origin = Protocol + Domain + Port
Ví dụ:
https://example.com:443
- origin 1http://example.com:443
- origin 2 (khác protocol)https://example.com:8080
- origin 3 (khác port)
Nguyên tắc SOP
Browser chỉ cho phép JavaScript từ origin A truy cập tài nguyên từ origin A. Cross-origin requests bị chặn theo mặc định.
// Code từ https://app.com
fetch('https://api.com/data') // Bị chặn bởi SOP
fetch('https://app.com/data') // OK - same origin
Vấn đề với SOP
SOP bảo vệ người dùng nhưng cũng tạo ra giới hạn không thực tế:
- Frontend và Backend thường deploy trên domain khác nhau
- Cần integrate với third-party APIs
- Microservices architecture với nhiều services độc lập
CORS - Giải pháp linh hoạt
CORS (Cross-Origin Resource Sharing) cho phép server chủ động "nới lỏng" SOP một cách có kiểm soát.
Cơ chế hoạt động
- Browser gửi request với Origin header:
GET /api/data HTTP/1.1
Host: api.com
Origin: https://app.com
- Server phản hồi với CORS headers:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.com
- Browser kiểm tra:
- Origin được phép → Cho phép JavaScript access response
- Origin không được phép → Block và throw CORS error
Simple Requests vs Preflight
Simple Requests (không cần preflight):
- Methods: GET, POST, HEAD
- Headers: Chỉ simple headers (Accept, Content-Language, Content-Type với một số giá trị)
- Content-Type: application/x-www-form-urlencoded, multipart/form-data, text/plain
Preflight Required cho:
- Methods khác: PUT, DELETE, PATCH
- Custom headers
- Content-Type: application/json
Preflight Request Flow
// 1. Browser gửi preflight
OPTIONS /api/data HTTP/1.1
Host: api.com
Origin: https://app.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, X-Custom-Header
// 2. Server response
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, X-Custom-Header
Access-Control-Max-Age: 86400
// 3. Browser gửi actual request (nếu được phép)
PUT /api/data HTTP/1.1
Host: api.com
Origin: https://app.com
Content-Type: application/json
X-Custom-Header: value
CORS Headers quan trọng
Response Headers (Server → Browser)
# Required
Access-Control-Allow-Origin: https://app.com | *
# For preflight
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400 # Cache preflight trong 24h
# Optional
Access-Control-Allow-Credentials: true # Cho phép gửi cookies
Access-Control-Expose-Headers: X-Total-Count # Expose custom headers cho JS
Lưu ý quan trọng
- Wildcard với Credentials:
# INVALID - Browser sẽ reject
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
# VALID
Access-Control-Allow-Origin: https://app.com
Access-Control-Allow-Credentials: true
- Dynamic Origin:
// Server-side
const allowedOrigins = ['https://app1.com', 'https://app2.com'];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
Debug CORS Issues
- Check Network Tab:
- Có preflight request không?
- Response headers có đúng không?
- Common Errors:
- "No 'Access-Control-Allow-Origin' header" → Server chưa config CORS
- "CORS header 'Access-Control-Allow-Origin' missing" → Check server logs
- "Credential is not supported if the CORS header 'Access-Control-Allow-Origin' is '*'" → Không dùng wildcard với credentials
- Tools:
curl -H "Origin: https://app.com" https://api.com/endpoint
- Browser DevTools Network tab
- Postman (nhưng lưu ý: Postman không enforce CORS)
Best Practices
- Nguyên tắc least privilege:
- Chỉ allow origins thực sự cần thiết
- Chỉ expose headers cần thiết
- Security considerations:
- Không dùng
Access-Control-Allow-Origin: *
cho sensitive APIs - Validate Origin header server-side
- Implement proper authentication (CORS không phải là security mechanism)
- Performance:
- Set
Access-Control-Max-Age
để cache preflight - Minimize preflight bằng cách dùng simple requests khi có thể
Kết luận
CORS không phải là "lỗi" mà là security feature. Hiểu rõ cách hoạt động sẽ giúp bạn:
- Debug nhanh hơn khi gặp issues
- Config server đúng cách
- Design API tốt hơn
Nhớ rằng: CORS là browser policy, không phải server policy. Server chỉ "suggest" thông qua headers, browser quyết định enforce.
Nguồn tham khảo: