Tư duy Feynman • Trực quan hình họa • Java / Spring Boot

Chương 06: Cross-Site Request Forgery — CSRF

Hóa giải tấn công mượn tay người dùng với Spring Security: Cấu hình CsrfFilter, đồng bộ CookieCsrfTokenRepository và cờ SameSite=Strict.

#01 Cross-Site Request Forgery — CSRF (Giả mạo yêu cầu chéo trang)

💡 BƯỚC 1: ẨN DỤ ĐỜI THƯỜNG (FEYNMAN ANALOGY)
Bạn vừa đăng nhập vào ngân hàng và vẫn đang giữ chiếc bút ký séc còn mực trên bàn. Bạn tiện tay mở một bức thư rác (trang web độc hại). Bức thư rác đó lén lút đưa một tờ giấy chuyển tiền 100 triệu vào dưới ngòi bút của bạn và dùng tay bạn ký xoẹt một cái (mượn quyền Cookie trình duyệt đang lưu). Ngân hàng thấy chữ ký thật và Cookie thật nên lập tức chuyển tiền mà bạn không hề hay biết!
📊 HÌNH HỌA MINH HỌA (VISUAL FLOW): Cross-Site Request Forgery — CSRF (Giả mạo yêu cầu chéo trang)
Victim Browser Lưu Cookie phiên Trang web lạ Lén submit Form POST Bank Server Nhận Cookie tự động Tài khoản Bị trừ tiền trái phép ⚠️ Lỗ hổng CSRF: Trình duyệt tự đính kèm Cookie của người dùng
🧪 BƯỚC 2: PHÒNG THÍ NGHIỆM TƯƠNG TÁC (SANDBOX)
Mẫu thử nghiệm: Tự động submit form ẩn từ trang lạThao tác hợp lệ từ chính trang bank.com
⚠️ Lỗ hổng CSRF: Trình duyệt tự động đính kèm Cookie phiên khi gửi request sang bank.com, khiến máy chủ tưởng rằng chính người dùng chủ động gửi lệnh!
⚖️ BƯỚC 3: SO SÁNH CODE LỖI VS CODE CHUẨN (JAVA / SPRING BOOT)
❌ Vulnerable Java Code (Không an toàn)
// THẢM HỌA: Tắt tính năng bảo vệ CSRF trên ứng dụng dùng Session Cookie
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.disable()); // CỰC KỲ NGUY HIỂM NẾU DÙNG COOKIE!
        return http.build();
    }
}
✅ Secure Java Code (Chuẩn bảo mật)
// CHUẨN AN TOÀN SPRING SECURITY 6: Kích hoạt CsrfFilter với CookieCsrfTokenRepository
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()) // Hỗ trợ SPA (React/Vue/Angular)
        );
        return http.build();
    }
}
🎯 BƯỚC 4: THỬ TÀI THẤU HIỂU (ACTIVE CHECK)
Bản chất cốt lõi khiến cuộc tấn công CSRF có thể xảy ra là gì?
A. Do máy tính của người dùng bị nhiễm virus trojan.
B. Do trình duyệt tự động gửi kèm Cookie xác thực mỗi khi tạo request tới một tên miền, bất kể request đó bắt nguồn từ trang web nào.
C. Do giao thức HTTPS bị hỏng.
D. Do server database bị tấn công SQL Injection.
📝 Tóm tắt nhanh bằng lời của bạn (Feynman Notebook): (Tự động lưu vào trình duyệt)

#02 Anti-CSRF Token (Mã chống giả mạo)

💡 BƯỚC 1: ẨN DỤ ĐỜI THƯỜNG (FEYNMAN ANALOGY)
CSRF Token giống như một chiếc 'Mật hiệu bí mật' mà ngân hàng ghé tai thì thầm riêng với bạn khi bạn mở trang giao dịch. Mật hiệu này được in vào một ô ẩn trong mẫu đơn (Hidden Field). Kẻ gian ở trang web lạ có thể xúi bạn nộp đơn, nhưng vì chính sách Same-Origin Policy (SOP), hắn không thể đọc trộm mật hiệu trong tai bạn. Ngân hàng nhận đơn thấy thiếu mật hiệu này sẽ xé bỏ tờ đơn ngay lập tức.
📊 HÌNH HỌA MINH HỌA (VISUAL FLOW): Anti-CSRF Token (Mã chống giả mạo)
Client Request Gửi form POST CSRF Header X-XSRF-TOKEN CsrfFilter So khớp Token với Session Spring Controller Chấp nhận giao dịch 🛡️ Synchronizer Token Pattern: Token bí mật ngẫu nhiên không thể đoán trước
🧪 BƯỚC 2: PHÒNG THÍ NGHIỆM TƯƠNG TÁC (SANDBOX)
Mẫu thử nghiệm: Token rỗngToken giả mạo: 'random-fake'X-CSRF-Token hợp lệ từ server
⚠️ Thiếu CSRF Token: Máy chủ không thể phân biệt request đến từ giao diện chính chủ hay từ trang web lừa đảo!
⚖️ BƯỚC 3: SO SÁNH CODE LỖI VS CODE CHUẨN (JAVA / SPRING BOOT)
❌ Vulnerable Java Code (Không an toàn)
// KHÔNG AN TOÀN: Endpoint thay đổi trạng thái chỉ kiểm tra cookie xác thực
@PostMapping("/api/transfer")
public ResponseEntity transferMoney(@RequestParam String toAccount, 
                                      @RequestParam BigDecimal amount, 
                                      HttpSession session) {
    Long userId = (Long) session.getAttribute("USER_ID");
    if (userId == null) return ResponseEntity.status(401).build();
    // Thực hiện giao dịch mà không kiểm tra token chống giả mạo
    accountService.transfer(userId, toAccount, amount);
    return ResponseEntity.ok("Success");
}
✅ Secure Java Code (Chuẩn bảo mật)
// CHUẨN AN TOÀN: Kiểm tra CSRF Token qua Request Header hoặc Form Param
@PostMapping("/api/transfer")
public ResponseEntity transferMoney(@RequestBody TransferRequest req,
                                      @AuthenticationPrincipal UserPrincipal user,
                                      HttpServletRequest request) {
    // Spring Security CsrfFilter tự động kiểm tra token trong Header X-XSRF-TOKEN
    // Nếu token thiếu hoặc sai lệch -> ném lỗi AccessDeniedException (403 Forbidden)
    accountService.transfer(user.getId(), req.getToAccount(), req.getAmount());
    return ResponseEntity.ok("Transfer Completed");
}
🎯 BƯỚC 4: THỬ TÀI THẤU HIỂU (ACTIVE CHECK)
Thuộc tính nào giúp bảo vệ CSRF Token không bị trang web của kẻ tấn công đọc lén?
A. Chính sách cùng nguồn gốc (Same-Origin Policy - SOP) của trình duyệt.
B. Băng thông mạng 5G.
C. Trình duyệt tự động nén dữ liệu.
D. Bộ nhớ đệm CPU.
📝 Tóm tắt nhanh bằng lời của bạn (Feynman Notebook): (Tự động lưu vào trình duyệt)

#03 SameSite Cookie Attribute (Cờ SameSite)

💡 BƯỚC 1: ẨN DỤ ĐỜI THƯỜNG (FEYNMAN ANALOGY)
Cờ SameSite trên Cookie giống như biển báo quy định: 'Vé này CHỈ ĐƯỢC PHÉP dùng khi bạn đang đứng trong sân nhà (Strict), hoặc khi bạn bấm link đi bộ từ ngoài cổng vào (Lax). Tuyệt đối cấm sử dụng vé nếu bạn đang ngồi trên chiếc xe của kẻ khác (Third-party context)'. Đây là giải pháp phòng ngự hiện đại cực mạnh được tích hợp sẵn ở cấp trình duyệt.
📊 HÌNH HỌA MINH HỌA (VISUAL FLOW): SameSite Cookie Attribute (Cờ SameSite)
Web lạ (Evil) Bấm link sang Ngân hàng SameSite=Strict Trình duyệt chặn Cookie Bank Server Request không có Cookie Giao dịch An toàn tuyệt đối 🛡️ SameSite Cookie: Trình duyệt từ chối đính kèm Cookie trong ngữ cảnh bên thứ 3
🧪 BƯỚC 2: PHÒNG THÍ NGHIỆM TƯƠNG TÁC (SANDBOX)
Mẫu thử nghiệm: SameSite=None (Không bảo vệ)SameSite=Lax (Bảo vệ mặc định)SameSite=Strict (Bảo vệ tối đa)
⚠️ SameSite=None: Cookie được gửi tự do trong mọi ngữ cảnh chéo trang (Cross-site), tạo điều kiện cho CSRF diễn ra!
⚖️ BƯỚC 3: SO SÁNH CODE LỖI VS CODE CHUẨN (JAVA / SPRING BOOT)
❌ Vulnerable Java Code (Không an toàn)
// CẤU HÌNH YẾU: Không thiết lập SameSite hoặc đặt SameSite=None
ResponseCookie cookie = ResponseCookie.from("SESSION_ID", sessionId)
    .httpOnly(true)
    .secure(true)
    .path("/")
    .sameSite("None") // Cho phép gửi chéo trang trong mọi tình huống!
    .build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
✅ Secure Java Code (Chuẩn bảo mật)
// CHUẨN AN TOÀN: application.properties hoặc ResponseCookie SameSite=Strict / Lax
// Trong application.properties:
server.servlet.session.cookie.same-site=strict
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true

// Hoặc tạo Cookie thủ công trong Spring Controller:
ResponseCookie secureCookie = ResponseCookie.from("SESSION_ID", sessionId)
    .httpOnly(true)
    .secure(true)
    .sameSite("Strict") // Chỉ gửi khi cùng miền nguồn gốc
    .maxAge(Duration.ofHours(2))
    .path("/")
    .build();
response.addHeader(HttpHeaders.SET_COOKIE, secureCookie.toString());
🎯 BƯỚC 4: THỬ TÀI THẤU HIỂU (ACTIVE CHECK)
Sự khác biệt chính giữa 'SameSite=Strict' và 'SameSite=Lax' là gì?
A. Strict chỉ dùng cho máy Mac, Lax dùng cho Windows.
B. Strict không bao giờ gửi cookie khi đi từ trang khác sang (kể cả click vào đường link thông thường), trong khi Lax vẫn cho phép gửi cookie khi click link GET thông thường từ bên ngoài.
C. Lax làm mã hóa mật khẩu hai chiều.
D. Không có điểm gì khác nhau.
📝 Tóm tắt nhanh bằng lời của bạn (Feynman Notebook): (Tự động lưu vào trình duyệt)

#04 Origin Header Validation (Xác thực tiêu đề Origin)

💡 BƯỚC 1: ẨN DỤ ĐỜI THƯỜNG (FEYNMAN ANALOGY)
Khi ai đó gửi kiện hàng đến nhà bạn, trên kiện hàng có con dấu niêm phong bất biến của bưu cục nơi xuất phát (Header Origin). Trình duyệt của người dùng tự động đóng con dấu này và mã nguồn JavaScript không thể giả mạo được con dấu đó. Nếu con dấu ghi nguồn xuất phát từ 'evil-hacker.com' thay vì 'mybank.com', bạn vứt kiện hàng vào sọt rác ngay lập tức.
📊 HÌNH HỌA MINH HỌA (VISUAL FLOW): Origin Header Validation (Xác thực tiêu đề Origin)
HTTP Request Đọc Header Origin Origin Matcher So khớp Allowlist domain Kết quả Chấp nhận hoặc 403 Forbidden 🛡️ Origin Header: Trình duyệt tự sinh, JavaScript phía client không thể giả mạo
🧪 BƯỚC 2: PHÒNG THÍ NGHIỆM TƯƠNG TÁC (SANDBOX)
Mẫu thử nghiệm: Origin: https://mybank.com (Chính chủ)Origin: https://evil-site.com (Tấn công)Origin: null (Truy cập từ file:// hoặc iframe cấm)
⚠️ Origin là 'https://evil-site.com': Yêu cầu được kích hoạt từ một trang web độc hại của hacker!
⚖️ BƯỚC 3: SO SÁNH CODE LỖI VS CODE CHUẨN (JAVA / SPRING BOOT)
❌ Vulnerable Java Code (Không an toàn)
// NGUY HIỂM: Tin tưởng mọi Origin hoặc chỉ so sánh chuỗi lỏng lẻo
String origin = request.getHeader("Origin");
if (origin != null && origin.contains("mybank.com")) {
    // Dễ bị bypass bằng evil-mybank.com hoặc mybank.com.attacker.com!
    allowRequest();
}
✅ Secure Java Code (Chuẩn bảo mật)
// CHUẨN AN TOÀN: Kiểm tra Origin bằng URI hoặc Allowlist nghiêm ngặt
private static final Set ALLOWED_ORIGINS = Set.of(
    "https://bank.com", 
    "https://app.bank.com"
);

public boolean isValidOrigin(HttpServletRequest request) {
    String origin = request.getHeader("Origin");
    if (origin == null) {
        return false; // Yêu cầu POST/PUT/DELETE bắt buộc phải có Origin
    }
    try {
        URI originUri = URI.create(origin);
        String normalizedOrigin = originUri.getScheme() + "://" + originUri.getAuthority();
        return ALLOWED_ORIGINS.contains(normalizedOrigin);
    } catch (IllegalArgumentException e) {
        return false;
    }
}
🎯 BƯỚC 4: THỬ TÀI THẤU HIỂU (ACTIVE CHECK)
Mã JavaScript độc hại chạy trên trình duyệt của kẻ tấn công có thể giả mạo hoặc thay đổi giá trị của Header 'Origin' khi gửi request bằng fetch() không?
A. Có, chỉ cần ghi đè trong options của fetch.
B. Không, Origin là một 'Forbidden Header' do trình duyệt hoàn toàn kiểm soát và bảo vệ, mã JavaScript không thể can thiệp được.
C. Chỉ thay đổi được nếu cài plugin trình duyệt.
D. Tùy thuộc vào tốc độ mạng.
📝 Tóm tắt nhanh bằng lời của bạn (Feynman Notebook): (Tự động lưu vào trình duyệt)

#05 Referer Header Validation (Xác thực tiêu đề Referer)

💡 BƯỚC 1: ẨN DỤ ĐỜI THƯỜNG (FEYNMAN ANALOGY)
Referer giống như dấu vết bánh xe để lại trên đường, cho biết bạn vừa lái xe từ con đường nào rẽ vào đây. Nếu một lệnh chuyển tiền nhạy cảm lại xuất hiện dấu vết xuất phát từ 'http://attacker-game.com/free-money', hệ thống nhận diện đây là hành vi lừa đảo.
📊 HÌNH HỌA MINH HỌA (VISUAL FLOW): Referer Header Validation (Xác thực tiêu đề Referer)
HTTP Request Lấy Referer Header Parse URI Trích xuất Host & Scheme Allowlist Check So khớp tuyệt đối Response Cho phép hoặc Hủy bỏ 🛡️ Referer Header: Xác thực nguồn gốc chuyển tiếp trang
🧪 BƯỚC 2: PHÒNG THÍ NGHIỆM TƯƠNG TÁC (SANDBOX)
Mẫu thử nghiệm: Referer: https://mybank.com/transfer (Chuẩn)Referer: https://mybank.com.attacker.com (Bypass lỏng lẻo)Referer rỗng (Referrer-Policy: no-referrer)
⚠️ Regex lỏng lẻo: Kiểm tra referer.includes('mybank.com') bị bypass dễ dàng bởi tên miền phụ của hacker: mybank.com.attacker.com!
⚖️ BƯỚC 3: SO SÁNH CODE LỖI VS CODE CHUẨN (JAVA / SPRING BOOT)
❌ Vulnerable Java Code (Không an toàn)
// NGUY HIỂM: Kiểm tra Referer bằng regex hoặc contains thiếu chặt chẽ
String referer = request.getHeader("Referer");
if (referer != null && referer.startsWith("http://mybank.com")) {
    // Bỏ sót HTTPS, hoặc kẻ tấn công dùng subdomain lừa đảo
    process();
}
✅ Secure Java Code (Chuẩn bảo mật)
// CHUẨN AN TOÀN: Parse Referer thành URL và so khớp Hostname tuyệt đối
public boolean isRefererTrusted(HttpServletRequest request) {
    String referer = request.getHeader("Referer");
    if (referer == null) return false;
    try {
        URI refUri = URI.create(referer);
        return "https".equalsIgnoreCase(refUri.getScheme()) &&
               "bank.com".equalsIgnoreCase(refUri.getHost()) &&
               (refUri.getPort() == -1 || refUri.getPort() == 443);
    } catch (Exception e) {
        return false;
    }
}
🎯 BƯỚC 4: THỬ TÀI THẤU HIỂU (ACTIVE CHECK)
Vì sao Referer Header không nên là biện pháp phòng thủ CSRF DUY NHẤT mà chỉ nên dùng làm lớp phòng thủ bổ trợ?
A. Vì Referer có thể bị người dùng chặn hoặc xóa bỏ hoàn toàn thông qua cấu hình Referrer-Policy hoặc trình duyệt bảo mật.
B. Vì Referer làm chậm tốc độ CPU.
C. Vì Referer chỉ có trong giao thức HTTP/1.0.
D. Vì Referer bị Google cấm.
📝 Tóm tắt nhanh bằng lời của bạn (Feynman Notebook): (Tự động lưu vào trình duyệt)