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?

CORS - Hiểu đúng để không còn 'sợ'

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:

  1. Sau khi login, browser lưu session cookie
  2. Mọi request tiếp theo đến bank.com đều tự động gửi kèm cookie này
  3. Server bank.com dựa vào cookie để xác thực người dùng

Vấn đề phát sinh khi:

  1. Bạn vào một trang web độc hại evil.com
  2. Trang này chứa code JavaScript gửi request đến bank.com/transfer
  3. Browser tự động gửi kèm session cookie (vì đây là request đến bank.com)
  4. 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 1
  • http://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

  1. Browser gửi request với Origin header:
GET /api/data HTTP/1.1
Host: api.com
Origin: https://app.com
  1. Server phản hồi với CORS headers:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.com
  1. 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

  1. 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
  1. 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

  1. Check Network Tab:
  • Có preflight request không?
  • Response headers có đúng không?
  1. 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
  1. 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

  1. Nguyên tắc least privilege:
  • Chỉ allow origins thực sự cần thiết
  • Chỉ expose headers cần thiết
  1. 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)
  1. 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:

  1. 3 ways to fix the CORS error and how Access-Control-Allow-Origin works
  2. MDN - Cross-Origin Resource Sharing (CORS)