Gần cuối một sê-ri về bảo mật, câu hỏi thật là: làm sao biết một ứng dụng đã cấu hình đúng? Không phải bằng cảm giác "chắc ổn", mà bằng một danh sách kiểm chạy được và cho ra kết quả nhị phân — đạt hay không. Bài này lấy một danh sách kiểm bảo mật cho Spring Boot rồi chạy nó trên một ứng dụng thật đang chạy: chính cái blog bạn đang đọc. Đây là ứng dụng của mình, kiểm chỉ-đọc (chỉ đọc header và mã trạng thái, không gửi payload tấn công), không nhắm vào hệ thống của ai khác. Và nó tìm ra một lỗ có thật.
Mỗi mục đo 3 lần cách nhau hơn một giây để không chạm giới hạn tần suất; kết quả giống hệt qua cả ba.
Những mục đã đạt
Header bảo mật. Danh sách kiểm bắt đầu bằng năm header mà một app HTTPS nghiêm túc phải gửi. Blog gửi đủ:
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.jsdelivr.net ...
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Mỗi cái đóng một cửa cụ thể. HSTS (một năm, kèm mọi tên miền con) buộc trình duyệt luôn dùng HTTPS, khoá cửa tấn công hạ cấp về HTTP. CSP với script-src không có unsafe-inline là tấm khiên chính chống XSS — dù kẻ tấn công chèn được <script> vào trang, trình duyệt cũng từ chối chạy. X-Frame-Options: SAMEORIGIN chặn clickjacking. nosniff cấm trình duyệt đoán kiểu nội dung. Referrer-Policy giới hạn thông tin rò qua header Referer. Năm cái này là phần khung, và app qua hết.
Chuyển hướng và endpoint quản trị. HTTP bị đẩy sang HTTPS bằng 301, đúng như mong đợi. Quan trọng hơn, tôi thử bốn endpoint actuator — cổng giám sát của Spring Boot mà nếu để hở sẽ lộ biến môi trường, cấu hình, thậm chí cho tải cả ảnh bộ nhớ (heapdump):
/actuator -> 404
/actuator/env -> 404
/actuator/health -> 404
/actuator/heapdump -> 404
Tất cả 404 — không lộ. Đây là một trong những lỗ hổng Spring Boot phổ biến nhất trong thực tế (vô số vụ rò rỉ bắt đầu từ một /actuator/env để công khai), nên thấy nó đóng kín là một mục đạt đáng kể. Điều đáng nói là mặc định của Spring Boot đời mới chỉ phơi /actuator/health ra ngoài, phần còn lại phải cố ý mở — nên đóng kín ở đây là giữ đúng mặc định an toàn chứ không phải thêm rào, đúng mô-típ lặp lại của cả sê-ri.
Trang lỗi. Tôi gọi một URL không tồn tại và kiểm phản hồi có lộ stack trace hay trang "Whitelabel Error" mặc định của Spring không — thứ hay tiết lộ tên lớp, phiên bản framework, cấu trúc bên trong. Kết quả: không lộ gì. Trang 404 sạch.
Mục không đạt: cookie phiên thiếu cờ Secure
Danh sách kiểm chỉ có giá trị nếu nó dám cho ra kết quả "không đạt". Và đây là mục đó:
Set-Cookie: JSESSIONID=...; Path=/; HttpOnly
HttpOnly? CO
Secure? khong
Cookie phiên JSESSIONID có cờ HttpOnly (tốt — JavaScript không đọc được nó, giảm rủi ro trộm phiên qua XSS), nhưng thiếu cờ Secure. Cờ Secure bảo trình duyệt chỉ gửi cookie qua HTTPS, không bao giờ qua HTTP thường. Thiếu nó, về lý thuyết cookie phiên có thể bị gửi qua một kết nối HTTP không mã hoá và bị nghe lén.
Thành thật về mức độ: ở đây rủi ro thực tế thấp, vì app đã bật HSTS — trình duyệt tuân thủ sẽ không bao giờ thử HTTP sau lần đầu, nên gần như không có cơ hội để cookie đi qua kênh thường. Nhưng "rủi ro thấp nhờ một lớp khác" không phải là "đạt". Cờ Secure là phòng thủ theo chiều sâu: nó bảo vệ ngay cả khi HSTS vì lý do nào đó không áp (lần truy cập đầu tiên trước khi HSTS được ghi nhớ, một tên miền con cấu hình lệch). Đây đúng là loại mục mà mắt thường bỏ qua — mọi thứ "trông an toàn", khoá xanh, header đầy đủ — nhưng một dòng kiểm tự động chỉ thẳng ra chỗ còn hở. Với Spring Boot, sửa nó là một dòng cấu hình: server.servlet.session.cookie.secure=true.
Phần danh sách kiểm không đo được từ ngoài
Những mục trên đo được chỉ bằng curl từ bên ngoài — đó là nửa danh sách kiểm nhìn thấy qua HTTP. Nửa còn lại cần đọc cấu hình và mã, và đáng liệt kê để bức tranh đủ:
- CSRF: form thay đổi trạng thái phải có token chống giả mạo yêu cầu. Spring Security bật sẵn cho phiên dựa trên cookie; API dùng token trong header thì miễn CSRF (stateless) — nhưng phải cố ý miễn, không phải vô tình tắt.
- Phân quyền theo vai: mỗi endpoint quản trị phải kiểm quyền ở tầng máy chủ, không dựa vào việc ẩn nút trên giao diện. Một
curlthẳng vào URL admin với tài khoản thường phải trả 403. - Giới hạn tần suất và khoá đăng nhập: các bài trước đã đo — bảo vệ như băm mật khẩu chậm sẽ thành vũ khí nếu không chặn số lần thử.
- Xác thực đầu vào và giới hạn kích thước: chặn thân request khổng lồ, giới hạn số bản ghi sinh ra từ một yêu cầu — cũng đã đo ở phần "bùng nổ tài nguyên".
- Quản lý bí mật và phụ thuộc: khoá không nằm trong mã, quét CVE của thư viện — hai phần riêng của sê-ri này.
Điểm chung: một danh sách kiểm tốt trộn cả mục đo từ ngoài (header, cookie, actuator) lẫn mục đọc từ trong (CSRF, phân quyền, xác thực). Chỉ làm một nửa thì bỏ sót nửa kia — giống đúng bài học SAST và DAST: mỗi góc nhìn thấy một phần, an toàn là chỗ giao của nhiều góc nhìn.
Ghép lại
Và một mục cuối, mức lưu ý: header Server: nginx/1.24.0 (Ubuntu) để lộ phiên bản máy chủ — một chút thông tin thừa giúp kẻ tấn công tra cứu lỗ hổng khớp phiên bản. Không nghiêm trọng, nhưng ẩn đi (server_tokens off) là việc dễ.
Tổng kết danh sách: tám mục đạt, một mục cần sửa (cờ Secure), một mục lưu ý (lộ phiên bản). Đó chính là giá trị của một danh sách kiểm — nó biến câu "ứng dụng này có an toàn không?" mơ hồ thành một bảng từng dòng đo được, và buộc ra ánh sáng cái mục mà sự tự tin chung sẽ che mất. Không có danh sách kiểm, cookie thiếu Secure kia sẽ nằm im mãi vì mọi thứ khác trông quá ổn.
Cả sê-ri quy về đây: từng bài đo một lỗ hổng riêng, còn danh sách kiểm là cách gộp chúng thành một quy trình lặp lại được. Điều đáng nhớ là không app nào "đạt hết mãi mãi" — cấu hình trôi, thư viện cập nhật, endpoint mới mọc ra. Danh sách kiểm không phải làm một lần; nó là thứ chạy lại đều đặn, đúng tinh thần "đo, đừng đoán" của toàn sê-ri.
Thử ba mươi giây
Chạy ba dòng này trên ứng dụng của chính bạn để kiểm ba mục quan trọng nhất:
# 1. Header bao mat co du khong?
curl -sI https://your-app/ | grep -iE 'strict-transport|content-security|x-frame|x-content-type|referrer'
# 2. Actuator co bi lo khong? (mong doi 404/401, KHONG phai 200)
curl -s -o /dev/null -w '%{http_code}\n' https://your-app/actuator/env
# 3. Cookie phien co Secure + HttpOnly khong?
curl -sI https://your-app/login | grep -i set-cookie
Nếu dòng ba hiện Set-Cookie mà không có chữ Secure, bạn vừa tìm ra đúng cái lỗ mà app này cũng mắc — và giờ bạn biết chính xác một dòng cần thêm để đóng nó.