Rất nhiều lỗ hổng web nghiêm trọng — clickjacking, một phần XSS, tấn công hạ cấp HTTPS — không cần sửa một dòng code ứng dụng nào để phòng: chỉ cần server gửi đúng vài header bảo mật. Chúng là các chỉ thị trong response nói cho trình duyệt "hãy siết chặt theo cách này". Điều khiến chúng dễ bị bỏ qua cũng chính là điều khiến chúng mạnh: người thi hành không phải server, mà là trình duyệt. Bài này (phần 18, khép lại loạt "HTTP và web đằng sau") xem các header thật mà chính blog này đang gửi, rồi dùng một trình duyệt Chromium thật để chứng kiến nó thi hành một header.
Bốn header cốt lõi và mỗi cái chặn gì
Strict-Transport-Security: max-age=31536000; includeSubDomains # ép HTTPS
Content-Security-Policy: default-src 'self'; script-src 'self' ... # chặn XSS
X-Frame-Options: SAMEORIGIN # chống clickjacking
X-Content-Type-Options: nosniff # chặn đoán kiểu file
- HSTS (
Strict-Transport-Security): sau lần đầu thấy header này, trình duyệt ghi nhớ rằng trongmax-agegiây tới (đây là một năm) chỉ được vào site qua HTTPS — kể cả khi người dùng gõhttp://. Nó chặn tấn công hạ cấp (kẻ tấn công ép kết nối về HTTP để nghe lén).includeSubDomainsmở rộng luật này cho mọi tên miền con. - CSP (
Content-Security-Policy): khai báo nguồn nào được phép nạp script, style, ảnh, iframe. Một script lạ bị chèn vào trang (XSS) mà không khớpscript-srcsẽ bị trình duyệt từ chối chạy. Đây là lớp phòng thủ XSS mạnh nhất ở tầng trình duyệt. X-Frame-Options/ CSPframe-ancestors: cấm site khác nhúng trang bạn vào<iframe>. Ngăn clickjacking — kẻ xấu phủ trang bạn dưới một lớp trong suốt rồi lừa người dùng bấm nhầm nút thật.X-Content-Type-Options: nosniff: cấm trình duyệt "đoán" kiểu nội dung khác vớiContent-Typeserver khai. Chặn kiểu tấn công tải lên file trông vô hại nhưng bị trình duyệt hiểu và chạy như HTML/JS.

Hình 1: Bốn header bảo mật cốt lõi — HSTS ép HTTPS, CSP khoá nguồn tài nguyên để chặn XSS, X-Frame-Options chống clickjacking, nosniff cấm đoán kiểu file — mỗi cái nhắm một lớp tấn công riêng.
Đo thật: header chính blog này đang gửi
Đây không phải ví dụ giả định — chính blog coffeecode.vn gửi đủ bộ. Lấy bằng curl -I:
$ curl -I https://coffeecode.vn/
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
Content-Security-Policy: default-src 'self'; script-src 'self' ...
frame-ancestors 'self'; object-src 'none'; base-uri 'self'; form-action 'self'
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Chú ý CSP ở đây có script-src 'self' không kèm 'unsafe-inline' — nghĩa là mọi <script> viết thẳng trong HTML hay thuộc tính onclick= đều bị chặn, buộc code phải nằm trong file .js riêng. Đây là lý do blog này không dùng script nội tuyến. Cộng thêm object-src 'none' (cấm Flash/plugin), base-uri 'self' và form-action 'self' (chặn cướp form).
Chứng kiến trình duyệt thi hành: X-Frame-Options thật
Header chỉ có nghĩa khi trình duyệt thực sự thi hành. Để không dừng ở mô tả, mình dựng một nginx phục vụ hai trang: open.html (không header) và deny.html (X-Frame-Options: DENY), rồi một trang cha nhúng cả hai vào <iframe>. Sau đó nạp trang cha bằng Chromium thật (headless) và đọc nội dung từng iframe:

Hình 2: Chạy thật — trang không header nạp vào iframe bình thường (f1 đọc được "TRANG MO..."); trang có X-Frame-Options: DENY bị rỗng (f2), kèm chính lời từ chối của Chromium: "Refused to display ... because it set 'X-Frame-Options' to 'deny'".
Kết quả rõ ràng và do trình duyệt tự thi hành, không phải mình mô tả:
f1(open.html, không header): iframe nạp thành công, đọc được nội dung"TRANG MO: cho phep nhung iframe".f2(deny.html,X-Frame-Options: DENY): iframe rỗng — Chromium từ chối hiển thị, và ghi console error nguyên văn:Refused to display 'http://127.0.0.1:8095/' in a frame because it set 'X-Frame-Options' to 'deny'.
Trang DENY có tồn tại và trả về 200 OK (curl lấy được bình thường) — nhưng trình duyệt không cho nó hiện trong khung của trang khác. Đó chính xác là cách clickjacking bị vô hiệu hoá: kẻ tấn công không thể phủ trang thật của bạn lên bẫy của họ.
Đánh đổi cần cân nhắc
CSP mạnh nhưng khó triển khai đúng — dùng report-only trước. Một CSP quá chặt sẽ chặn nhầm chính tài nguyên hợp lệ của bạn (font, analytics, ảnh CDN) và trang vỡ âm thầm. Cách an toàn: bật Content-Security-Policy-Report-Only trước, thu thập báo cáo vi phạm để biết cần cho phép nguồn nào, rồi mới chuyển sang chế độ thực thi. Và nhớ: mọi thay đổi CSP phải kiểm trên trình duyệt thật vì công cụ test phía server thường không thi hành CSP.
HSTS khó thu hồi — cân nhắc trước khi bật includeSubDomains và preload. Vì trình duyệt nhớ HSTS tới một năm, nếu lỡ bật cho một tên miền con chưa sẵn sàng HTTPS, người dùng sẽ không vào được và bạn không thể "gỡ nhanh" — phải đợi max-age hết hoặc người dùng tự xoá. Bắt đầu với max-age nhỏ, tăng dần khi chắc chắn.
Đừng đặt cùng header ở hai tầng. Nếu cả nginx lẫn ứng dụng cùng gửi CSP hay HSTS, response có hai giá trị cho một header — sửa một chỗ quên chỗ kia rất khó lần. Chọn một tầng chịu trách nhiệm (blog này để ứng dụng tự gửi, nginx không đụng vào) và giữ nguyên tắc đó.
Ba ý mang về
- Header bảo mật chặn cả lớp tấn công mà không cần sửa code ứng dụng: HSTS ép HTTPS (chống hạ cấp), CSP khoá nguồn tài nguyên (chống XSS), X-Frame-Options chống clickjacking,
nosniffcấm đoán kiểu file — người thi hành là trình duyệt. - Đây là cấu hình thật, không lý thuyết: đo thật header
coffeecode.vngửi, vớiscript-src 'self'khôngunsafe-inline,object-src 'none',frame-ancestors 'self'— nên mọi script phải nằm trong file riêng. - Trình duyệt thật sự thi hành: đo bằng Chromium thật, trang có
X-Frame-Options: DENYbị từ chối hiển thị trong iframe (console: "Refused to display ... to 'deny'") dù vẫn trả200— đó là clickjacking bị vô hiệu hoá tận gốc.
Nguồn
- MDN Web Docs — HTTP headers for security: https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides
- MDN Web Docs — Content-Security-Policy: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
- MDN Web Docs — Strict-Transport-Security: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
- OWASP — HTTP Security Response Headers Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html
Đến đây khép lại loạt "HTTP và web đằng sau" — từ dòng request đầu tiên, status code, keep-alive, HTTP/2, cache, cookie, nén, TLS và X.509, reverse proxy, CORS, xác thực, range request, WebSocket, cho tới các header bảo mật ở bài này. Điểm chung xuyên suốt: HTTP không phải hộp đen — mỗi hành vi của trình duyệt đều bắt nguồn từ một header hay một quy tắc bạn có thể đo, kiểm và hiểu bằng curl cùng một trình duyệt thật. Nắm được lớp này, bạn debug web nhanh hơn và thiết kế hệ thống an toàn hơn.