Web Security for Frontend Devs · Part 8 — Secure Headers & HTTPS/TLS
HTTPS/TLS, HSTS, mixed content, the production security-header stack (CSP, COOP/COEP, Permissions-Policy, SRI), static-host `_headers` examples, and audit tooling — with exercises.
Phần 8/10 trong series Web Security for Frontend Devs. Trước: Tiếp:
Đây là Phần 8 của series 10 bài về những kiến thức bảo mật web mà mọi frontend dev nên biết — và chủ động phòng tránh. Mỗi phần giải thích một mối đe dọa thật, cho xem code lỗ hổng, rồi cách sửa, và kết thúc bằng bài tập.
Phần 7 khóa ai được nhúng UI bằng frame-ancestors và X-Frame-Options. Phần này là nền transport và header: HTTPS mọi nơi, HSTS, phần còn của bộ header bảo mật, và SRI để CDN bị xâm không đổi script lặng lẽ. Phần lớn là header phản hồi HTTP set tại host, CDN, hoặc reverse proxy — không chỉ gắn trong React.
HTTPS và TLS: sàn, không phải tính năng
TLS (người ta vẫn gọi “SSL”) cho ba tính chất trên đường truyền giữa trình duyệt và server:
- Bảo mật nội dung — ciphertext trên mạng; kẻ nghe lén không đọc được body hay cookie.
- Toàn vẹn — sửa đổi bị phát hiện; attacker trên đường không thay JS của bạn bằng malware lặng lẽ.
- Xác thực — chứng chỉ chứng minh bạn tới đúng host thật (trừ cert sai cấp và lỗi người dùng).
Không HTTPS, ai đó trên cùng Wi‑Fi, đường ISP, hay router bị xâm có thể đọc và sửa traffic — kể cả chèn <script> vào HTML trên đường. HTTP thuần đã lỗi thời cho site có đăng nhập, thanh toán, PII, hay admin; coi http:// chỉ là đích redirect, không phải URL app chính thức.
Frontend vẫn quan trọng: dùng URL tương đối hoặc https:// trong markup, bật HTTPS tự động trên static host, không ship link asset http:// trên production.
HSTS: ép HTTPS sau lần ghé đầu
Lần request đầu tới site vẫn có thể là http:// nếu user gõ hostname, bookmark cũ, hay link downgrade. HSTS bảo trình duyệt: với host này (và tùy chọn subdomain), chỉ dùng HTTPS trong max-age giây tới.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
| Piece | Vai trò |
|---|---|
max-age | Thời gian trình duyệt nhớ “chỉ HTTPS” |
includeSubDomains | Áp cho subdomain |
preload | Đủ điều kiện preload list trong trình duyệt — lần đầu không qua HTTP |
SSL stripping là tấn công HSTS chặn: attacker chặn lần http:// đầu và proxy HTTP trong khi nói HTTPS phía trên. Có HSTS, trình duyệt tuân thủ nâng cấp hoặc chặn trước khi form đăng nhập tải qua cleartext.
Lưu ý preload:gửi hstspreload.org khó hoàn tác — phải chạy HTTPS đúng trên mọi host trong danh sách hàng tháng trước khi gỡ lan rộng. Chỉ thêm preload khi mọi subdomain sẵn sàng và bạn kiểm soát TLS end-to-end.
Mixed content: trang HTTPS, tài nguyên HTTP
Trang tải qua https:// không được kéo nội dung active qua http://. Trình duyệt phân chặn được (script, stylesheet, iframe, XHR) hay nâng cấp/hiển thị (image, audio ở chế độ hiện đại).
Mẫu lỗ hổng
<script src="http://cdn.example.com/lib.js"></script>
<link rel="stylesheet" href="http://cdn.example.com/app.css" />
Sửa:URL theo protocol vẫn sai trên HTTPS; dùng HTTPS hoặc path tương đối gốc:
<script src="https://cdn.example.com/lib.js"></script>
<link rel="stylesheet" href="/assets/app.css" />
Gửi upgrade-insecure-requests trong CSP (xem Phần 3) để link http:// cũ tự nâng cấp khi có thể:
Content-Security-Policy: upgrade-insecure-requests; default-src 'self'
Audit bằng panel Issues DevTools và HTML checker CI — lỗi mixed content hay chỉ lộ ở staging khi ai đó copy URL embed cũ.
Bộ header bảo mật
Các header này không thay CSP Phần 3, framing Phần 7, hay validate server — chúng xếp chồng default trình duyệt độc lập.
CSP (tóm tắt)
CSP là tường lửa trong trình duyệt cho script, style, frame, và đích mạng — chi tiết ở Phần 3. Thói quen production tối thiểu: default-src 'self', script-src chặt với nonce/hash, object-src 'none', và frame-ancestors khớp Phần 7.
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-PLACEHOLDER'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
X-Content-Type-Options
Chặn trình duyệt đoán MIME response thành ngữ cảnh thực thi (vd upload text/plain thành HTML).
X-Content-Type-Options: nosniff
Kèm Content-Type đúng trên download và API; nosniff rẻ và hỗ trợ rộng.
Referrer-Policy
Điều khiển mức URL trước (path, query, token trong query) rò rỉ khi điều hướng và request tài nguyên phụ.
Referrer-Policy: strict-origin-when-cross-origin
Điều hướng same-origin gửi URL đầy đủ; cross-origin HTTPS chỉ gửi origin; hạ xuống HTTP không gửi referrer. Tùy chọn chặt hơn (no-referrer, same-origin) hữu ích trên URL OAuth và reset mật khẩu có secret trong query.
Framing
Phần 7 là nguồn chính cho clickjacking; triển khai cả CSP hiện đại và XFO legacy khi migrate:
Content-Security-Policy: frame-ancestors 'none'
X-Frame-Options: DENY
Route chỉ embed dùng allowlist hẹp trên route đó — không SAMEORIGIN toàn site nếu admin phải DENY.
Permissions-Policy
Trước là Feature-Policy — tắt API mạnh mặc định (camera, mic, geolocation, payment, usb, …) trừ khi bạn cho phép rõ:
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Dùng () để tắt mọi origin; self hoặc (https://trusted.example) để cho phép hẹp. Bổ sung CSP: CSP nói thứ gì được nạp; Permissions-Policy nói thứ gì được chạy trong trang.
Cô lập cross-origin
Tấn công kiểu Spectre đọc bộ nhớ cross-origin trong cùng process. Trình duyệt giảm nhẹ bằng cô lập cross-origin — process riêng (hoặc biên chặt) để dữ liệu nhạy cảm không nằm cạnh nội dung attacker kiểm soát.
| Header | Mục đích |
|---|---|
Cross-Origin-Opener-Policy: same-origin | Context duyệt mới không giữ tham chiếu chung tới cửa sổ bạn cross-origin |
Cross-Origin-Embedder-Policy: require-corp | Tài nguyên cross-origin phải opt-in qua CORP hoặc CORS, không thì không nạp |
Cross-Origin-Resource-Policy: same-origin | Asset khai báo ai được nhúng/tiêu thụ trong ngữ cảnh cross-origin |
Khi COOP + COEP đúng, window.crossOriginIsolated === true và API như SharedArrayBuffer cùng timer độ phân giải cao lại khả dụng theo quy tắc kiểm soát. Nâng cao; chỉ áp khi cần API đó hoặc cô lập rõ — COEP cấu hình sai gãy widget bên thứ ba không gửi CORP.
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Resource-Policy: same-origin
Cache-Control trên phản hồi nhạy cảm
Trình duyệt và CDN có thể lưu HTML hoặc JSON đã xác thực trên cache đĩa — rủi ro thiết bị dùng chung.
Cache-Control: no-store
Pragma: no-cache
Asset marketing tĩnh vẫn max-age dài + immutable; tách policy theo route.
SRI
Thẻ script từ CDN chạy với toàn quyền origin bạn (Phần 1). CDN hoặc path bị xâm, attacker đổi file — SRI bắt trình duyệt băm byte và từ chối lệch.
<script
src="https://cdn.example.com/lib/v3.2.1/app.min.js"
integrity="sha384-PLACEHOLDER_BASE64_DIGEST"
crossorigin="anonymous"
></script>
Sinh digest lúc build (openssl dgst -sha384 -binary file.js | openssl base64 -A hoặc plugin bundler). crossorigin="anonymous" bắt buộc cho fetch CDN bật CORS dùng trong kiểm integrity. SRI không giúp script inline — kèm CSP hash/nonce (Phần 3). Chuỗi supply-chain tiếp ở Phần 9.
Ví dụ gộp: _headers static host
Nhiều static host (Netlify, Cloudflare Pages, v.v.) map file _headers hoặc config headers sang header phản hồi — giống add_header nginx hoặc rule CDN. Vài header không set ổn qua <meta http-equiv> (gồm HSTS và thực tế framing/isolation) — cấu hình ở edge.
/*
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Cross-Origin-Opener-Policy: same-origin
X-Frame-Options: DENY
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-PLACEHOLDER'; style-src 'self' 'nonce-PLACEHOLDER'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; upgrade-insecure-requests
Cache-Control: public, max-age=0, must-revalidate
/account/*
Cache-Control: no-store
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-PLACEHOLDER'; frame-ancestors 'none'
Chỉnh CSP theo môi trường; bắt đầu Content-Security-Policy-Report-Only trước khi enforce (Phần 3).
Báo cáo và công cụ audit
Scan externally after deploy:
- điểm chữ trên header phổ biến.
- hướng dẫn HTTP/TLS và header rộng hơn.
Báo cáo vi phạm CSP — gửi tới collector khi ở chế độ report-only:
Content-Security-Policy-Report-Only: script-src 'self'; report-uri https://reports.example.com/csp
Stack hiện đại ưu tiên Reporting-Endpoints + report-to (kế thừa Report-To cũ); kiểm tra CDN hỗ trợ trước khi phụ thuộc production. Cảnh báo khi spike — vi phạm script-src đột ngột thường là thử XSS hoặc deploy lỗi.
Checklist frontend
Trước khi coi series bảo mật “đã deploy”:
- URL chính là
https://; HTTP redirect 301 sang HTTPS; HSTS bật vớimax-agehợp lý. - Không có script/stylesheet/iframe
http://production; CSP cóupgrade-insecure-requestsnếu còn URL cũ. - Stack header ở CDN/server, không chỉ meta framework.
- CSP Phần 3 enforce (hoặc report-only có monitor); framing Phần 7 trên route nhạy cảm.
- Script CDN có SRI +
crossorigin; lockfile và review dependency ở Phần 9. - Phản hồi account và API JSON gửi
Cache-Control: no-storekhi cần. - Quét định kỳ securityheaders.com / Observatory sau đổi infra.
Bài tập / Exercises
1. Mỗi thứ một câu: confidentiality, integrity, authentication của TLS chống gì trên Wi‑Fi công cộng?
Lời giải
Confidentiality — kẻ nghe lén không đọc cookie phiên hay body phản hồi. Integrity — attacker không đổi bundle JS thành malware mà trình duyệt không phát hiện. Authentication — bạn nói chuyện với server sở hữu cert cho hostname đó, không phải kẻ giả danh local tầm thường.
2. Site thêm HSTS như trên nhưng http://legacy.example.com vẫn phục vụ admin qua HTTP. Gì hỏng, thứ tự sửa?
Lời giải
Trình duyệt có HSTS (nhất preload) từ chối hoặc nâng cấp legacy.example.com — admin không vào được hoặc lệch hành vi. Thứ tự sửa: (1) HTTPS trên legacy với cert hợp lệ, (2) xác minh mọi subdomain qua HTTPS, (3) deploy HSTS không preload đến khi ổn, (4) mới cân nhắc preload.
3. Thêm SRI cho script CDN dưới và nêu một directive CSP vẫn chặn inline nếu CDN sạch nhưng XSS chèn <script>alert(1)</script>.
<script src="https://cdn.example.com/analytics.js"></script>
Lời giải
<script
src="https://cdn.example.com/analytics.js"
integrity="sha384-PLACEHOLDER_DIGEST_FROM_BUILD"
crossorigin="anonymous"
></script>Ví dụ CSP: script-src 'self' 'nonce-PLACEHOLDER' — chặn inline alert dù file ngoài khớp hash.
Nâng cao:Giải thích vì sao COEP require-corp có thể gãy iframe thanh toán bên thứ ba, và nêu hai header bài này mà Phần 7 đã dùng cho mối đe dọa khác.
Lời giải
COEP require-corp bắt mọi tài nguyên cross-origin (kể cả frame lồng) gửi CORP hoặc CORS đúng; nhiều widget thanh toán là embed cũ không CORP nên không nạp. Phần 7 đã dùng frame-ancestors / X-Frame-Options chống clickjacking — cùng họ header, mục tiêu khác cô lập COEP. Content-Security-Policy frame-ancestors cũng xuất hiện ở hai phần — framing so với quy tắc script/nạp.
Điểm chính
- HTTPS/TLS bắt buộc: confidentiality, integrity, authentication; HTTP cleartext cho nghe lén và injection.
- HSTS đóng cửa sổ downgrade lần đầu;
preloadmạnh và chậm hoàn tác. - Mixed content và asset
http://hardcode phá trang HTTPS — dùng URL tương đối/https://vàupgrade-insecure-requests. - Xếp nosniff, Referrer-Policy, Permissions-Policy, CSP (Phần 3), framing (Phần 7),
Cache-Control: no-storetrên dữ liệu nhạy cảm. - COOP/COEP/CORP bật cô lập cross-origin và
crossOriginIsolated— áp có chủ đích; có thể gãy embed bên thứ ba. - SRI ghim byte script CDN; kèm CSP và thói quen supply-chain Phần 9.
- Cấu hình header trên server/CDN (
_headers, nginx, v.v.); audit bằng securityheaders.com, Observatory, và báo cáo CSP.
Tiếp theo
Phần 9 — Secrets, Data Leakage & Supply-Chain: biến môi trường trong bundle client, rò localStorage, typosquat npm, lockfile, và biến SRI + CSP thành pipeline release lặp lại.