TungDaDev's Blog

Một trong những ngộ nhận tai hại nhất của các kỹ sư phần mềm là nghĩ rằng CORS (Cross-Origin Resource Sharing) là một bức tường lửa (Firewall) bảo vệ Server khỏi các cuộc tấn công từ bên ngoài. Thực tế hoàn toàn ngược lại: CORS là cơ chế do Trình duyệt (Browser) thực thi để NỚI LỎNG chính sách Same-Origin Policy (SOP). Các công cụ như cURL, Postman, Mobile Apps hay Script tấn công của Hacker hoàn toàn phớt lờ CORS. Coi CORS là công cụ xác thực (Authentication) cho API là một sai lầm chết người về mặt an ninh thông tin.

Trong kiến trúc hiện đại, khi Frontend (Single Page Applications - React/Vue/Next.js) được deploy trên một tên miền (https://app.company.com) và Backend REST/GraphQL APIs nằm trên một tên miền hoặc cổng khác (https://api.company.com), lỗi No 'Access-Control-Allow-Origin' header is present on the requested resource là rào cản đầu tiên mọi lập trình viên đều gặp phải.

Tuy nhiên, thay vì hiểu đúng bản chất, nhiều đội ngũ chọn giải pháp nguy hiểm: Cấu hình Access-Control-Allow-Origin: * hoặc phản chiếu động (Reflect) bất kỳ Origin nào gửi lên. Bài viết này phân tích cơ chế bảo mật trình duyệt từ gốc rễ và cung cấp Bản thiết kế Hardening CORS cấp doanh nghiệp tại tầng API Gateway.


# nền tảng: same-origin policy (sop) & nghịch lý trình duyệt

Mọi trình duyệt hiện đại (Chrome, Safari, Firefox) đều bị chi phối bởi quy tắc an ninh nền tảng nhất: Same-Origin Policy (SOP) được phát minh bởi Netscape vào năm 1995.

Hai URL được coi là Cùng Nguồn (Same-Origin) khi và chỉ khi cả 3 yếu tố sau hoàn toàn trùng khớp 100%:

  1. Giao thức (Protocol/Scheme): http vs https → Khác nguồn.
  2. Tên miền máy chủ (Host/Domain): company.com vs api.company.com → Khác nguồn.
  3. Cổng mạng (Port): localhost:3000 vs localhost:8080 → Khác nguồn.

# tại sao sop lại quan trọng?

Nếu không có SOP:

  1. Bạn đăng nhập vào trang web ngân hàng my-bank.com, ngân hàng lưu session cookie vào trình duyệt.
  2. Bạn mở một tab khác vào trang web độc hại evil-site.com.
  3. Đoạn mã JavaScript trên evil-site.com có thể tự do gửi Ajax request sang my-bank.com/api/transfer-money, trình duyệt tự động đính kèm cookie đăng nhập của bạn, và tiền của bạn biến mất!

CORS sinh ra để giải quyết nhu cầu chính đáng: Cho phép my-bank-frontend.com được phép truy cập hợp pháp vào tài nguyên của my-bank-api.com dưới sự đồng thuận minh bạch của máy chủ.


# giải mã yêu cầu thăm dò: preflight request (HTTP options)

Không phải mọi request cross-origin đều gửi thẳng tới backend. Trình duyệt chia các request thành 2 nhóm: Simple Requests và Preflighted Requests.

# điều kiện kích hoạt preflight options

Request sẽ bị ép phải chạy Preflight nếu vi phạm bất kỳ điều kiện nào sau đây:

  • Sử dụng các HTTP Method khác ngoài: GET, HEAD, POST.
  • Sử dụng Content-Type khác ngoài: application/x-www-form-urlencoded, multipart/form-data, text/plain. (Lưu ý: Mọi request dùng application/json đều BẮT BUỘC phải Preflight!)
  • Có chứa bất kỳ Custom Headers nào (ví dụ: Authorization, X-Trace-Id, X-Tenant-Id).

# chi phí ẩn về hiệu năng: double round-trip time (2x rtt)

Mỗi lần frontend gọi một API POST/PUT JSON, trình duyệt phải thực hiện 2 lần bắt tay mạng liên tiếp:

  1. OPTIONS round-trip (~50ms - 200ms).
  2. POST/PUT payload round-trip (~50ms - 200ms). → Làm tăng gấp đôi P99 Latency của các tương tác người dùng!

Tối ưu hóa của Architect: Luôn trả về header Access-Control-Max-Age: 86400 (Cache kết quả Preflight trong 24 giờ). Trình duyệt sẽ lưu lại quyền truy cập và bỏ qua bước gửi OPTIONS cho toàn bộ các request tiếp theo tới cùng endpoint.


# các lỗ hổng bảo mật nguy hiểm khi cấu hình sai CORS

# cạm bẫy wildcard * đi kèm credentials

Rất nhiều lập trình viên thử cấu hình:

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

Trình duyệt sẽ chặn đứng cấu hình này ngay lập tức! Theo tiêu chuẩn bảo mật W3C/WHATWG, nếu máy chủ cho phép gửi thông tin định danh nhạy cảm (credentials: true — cookies, HTTP auth), thì Access-Control-Allow-Origin bắt buộc phải là một domain cụ thể, tuyệt đối không được dùng dấu sao *.

# lỗ hổng dynamic origin reflection (phản xạ mù quáng)

Để "lách" quy tắc trên, nhiều backend developer viết đoạn code tai hại:

// LỖ HỔNG AN NINH NGHIÊM TRỌNG:
String origin = request.getHeader("Origin");
response.setHeader("Access-Control-Allow-Origin", origin); // Phản chiếu bất kỳ ai gửi lên!
response.setHeader("Access-Control-Allow-Credentials", "true");
  • Kịch bản khai thác: Hacker tạo trang https://attacker.com. Khi người dùng truy cập trang này, script của hacker gửi request sang API của bạn với Origin: https://attacker.com.
  • Server của bạn ngây thơ đọc header và trả về Access-Control-Allow-Origin: https://attacker.com kèm credentials: true.
  • Trình duyệt cho phép JavaScript của hacker đọc toàn bộ response nhạy cảm (thông tin số dư tài khoản, email, token)!

# phân biệt rạch ròi: CORS vs CSRF

Tiêu chuẩnCORS (Cross-Origin Resource Sharing)CSRF (Cross-Site Request Forgery)
Bản chấtCơ chế cho phép Frontend ĐỌC KẾT QUẢ (Read Access) từ domain khácKẻ tấn công lợi dụng cookie để THỰC THI HÀNH ĐỘNG (Write/State Mutation)
Hành vi trình duyệtVẫn gửi request lên server, nhưng chặn không cho JS đọc response nếu thiếu header CORSGửi request và server thực thi bình thường! (Hacker không cần đọc kết quả, chỉ cần tiền bị trừ)
Biện pháp phòng thủCấu hình đúng Whitelist OriginSử dụng SameSite=Strict/Lax Cookies hoặc Anti-CSRF Tokens

# kiến trúc tập trung hóa: CORS hardening tại API gateway

Trong kiến trúc Microservices, việc phân tán cấu hình CORS bằng các annotation @CrossOrigin rải rác trên từng Controller của từng service là một thảm họa quản trị: Dễ bỏ sót, khó audit và không thể cập nhật tập trung.

Chuẩn mực Enterprise: Chặn và xử lý 100% Preflight và CORS tại tầng API Gateway (Spring Cloud Gateway / Envoy / NGINX) trước khi request chạm tới các microservices nghiệp vụ bên trong.

# cấu hình Spring cloud gateway chuẩn doanh nghiệp

spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            # Whitelist nghiêm ngặt các domain frontend tin cậy
            allowed-origin-patterns:
              - 'https://tungdadev.com'
              - 'https://*.tungdadev.com'
              - 'http://localhost:[3000,5173]' # Chỉ mở cho local dev
            allowed-methods:
              - GET
              - POST
              - PUT
              - DELETE
              - PATCH
              - OPTIONS
            allowed-headers:
              - Authorization
              - Content-Type
              - X-Requested-With
              - X-Correlation-Id
            exposed-headers:
              - Content-Disposition
              - X-Total-Count
            allow-credentials: true
            # Cache preflight trong 24 giờ để giảm thiểu latency cho người dùng
            max-age: 86400

# xử lý tại tầng Spring security 6+ trong service đơn lẻ

Nếu không có Gateway, cấu hình chặt chẽ trong SecurityFilterChain:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
 
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            // Bật CSRF protection nếu dùng Session Cookies; tắt nếu dùng stateless JWT Authorization header
            .csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // Luôn cho phép preflight đi qua
                .anyRequest().authenticated()
            )
            .build();
    }
 
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOriginPatterns(List.of("https://*.tungdadev.com", "http://localhost:3000"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        config.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Requested-With"));
        config.setAllowCredentials(true);
        config.setMaxAge(Duration.ofHours(24));
 
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
    }
}

# bảng kiểm tra an ninh vận hành (security architect's checklist)

  • 1. Không Dùng Wildcard Với Credentials: Đảm bảo không tồn tại đồng thời Access-Control-Allow-Origin: * và Access-Control-Allow-Credentials: true.
  • 2. Không Phản Chiếu Mù Quáng (No Blind Reflection): Mã nguồn tuyệt đối không lấy trực tiếp giá trị header request.getHeader("Origin") để gán vào Allow-Origin mà không qua whitelist kiểm tra.
  • 3. Cấu Hình Preflight Cache (max-age): Thiết lập Access-Control-Max-Age tối thiểu 3.600 giây (khuyến nghị 86.400 giây) để loại bỏ gánh nặng latency của các lệnh OPTIONS lặp lại.
  • 4. Chặn Preflight Tại Gateway: Đảm bảo các request OPTIONS được API Gateway phản hồi ngay lập tức (204 No Content), không được chuyển tiếp sâu vào các microservices nội bộ làm tốn tài nguyên worker threads.
  • 5. Phòng Thủ CSRF Toàn Diện: Nếu ứng dụng dùng Cookies để xác thực, phải kích hoạt cờ SameSite=Lax hoặc SameSite=Strict trên toàn bộ cookies phiên, kết hợp kiểm tra Origin / Referer headers ở các API làm biến đổi dữ liệu.

# lời kết

Bảo mật trình duyệt là phòng tuyến đầu tiên bảo vệ người dùng và tài nguyên doanh nghiệp trước các mối đe dọa không gian mạng. Hiểu đúng Same-Origin Policy, nắm vững cơ chế Preflight OPTIONS và thiết lập một cấu hình CORS Whitelist nghiêm ngặt tại API Gateway chính là nền tảng cốt lõi để xây dựng những hệ thống web hiện đại: vừa an toàn trước các cuộc tấn công đánh cắp dữ liệu, vừa mang lại trải nghiệm tương tác mượt mà và tối ưu nhất cho người dùng cuối.


Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!

Happy coding 😎 👍🏻 🚀 🔥.

← Previous postjava virtual threads
Next post →java 21 feature