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)
🧪 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ì?
📝 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)
🧪 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?
📝 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)
🧪 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ì?
📝 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)
🧪 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?
📝 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)
🧪 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ợ?
📝 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)