Mục tiêu học tập
- Hiểu vai trò các security headers quan trọng: CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy
- Cấu hình Content-Security-Policy (CSP) với nonce/hash, dùng report-only mode
- Áp dụng Subresource Integrity (SRI) khi load CDN
- Đo và improve security posture bằng
securityheaders.comvà Mozilla Observatory - Biết cách triển khai headers an toàn ở Express, Next.js, nginx
1. Vì sao Security Headers quan trọng?
Security headers là HTTP response headers báo cho browser cách xử lý nội dung an toàn hơn. Chúng:
- Là defense in depth — vẫn chạy kể cả khi có bug khác
- Free — chỉ cần config server, không sửa code business logic
- Khó cài sai gây regression (nếu bật từ đầu) so với refactor code
Tuy không thay thế được code an toàn, headers giảm impact khi có bug.
2. Bảng tổng quan headers quan trọng
| Header | Mục đích | Giá trị khuyến nghị |
|---|---|---|
| Strict-Transport-Security (HSTS) | Force HTTPS, chống SSL stripping | max-age=31536000; includeSubDomains; preload |
| Content-Security-Policy (CSP) | Whitelist nguồn resource, chặn XSS | default-src 'self'; script-src 'self' 'nonce-...' |
| X-Frame-Options | Chống clickjacking | DENY hoặc SAMEORIGIN |
| X-Content-Type-Options | Chống MIME sniffing | nosniff |
| Referrer-Policy | Kiểm soát Referer leak | strict-origin-when-cross-origin |
| Permissions-Policy | Bật/tắt browser feature | camera=(), microphone=(), geolocation=() |
| Cross-Origin-Opener-Policy (COOP) | Isolation top window | same-origin |
| Cross-Origin-Embedder-Policy (COEP) | Isolation embed resource | require-corp |
| Cross-Origin-Resource-Policy (CORP) | Chống cross-origin read | same-origin |
| DEPRECATED | Không dùng — bị remove khỏi browser hiện đại |
3. Strict-Transport-Security (HSTS)
Báo browser luôn dùng HTTPS cho domain này, kể cả khi user gõ
http://. Chống SSL stripping attack (MITM downgrade HTTPS → HTTP).
Format
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
| Directive | Ý nghĩa |
|---|---|
max-age=N | Số giây browser cache policy (31536000 = 1 năm) |
includeSubDomains | Áp dụng cho mọi subdomain |
preload | Cho phép submit vào HSTS preload list của Chrome/Firefox |
HSTS Preload
Sau khi domain submit và được accept vào hstspreload.org:
- Browser hard-code domain → HTTPS từ lần đầu, không cần fetch trước
- Loại trừ TOFU (Trust On First Use) gap
Cẩn thận: HSTS preload rất khó remove (mất tháng/năm để rút khỏi list). Chỉ submit khi chắc chắn site dùng HTTPS lâu dài.
Pitfalls
- Đặt
max-age=0để tắt HSTS (debug); production luôn1 năm includeSubDomainscó thể break subdomain cũ vẫn HTTP- Test trên staging trước; lỡ đặt sai, browser cache lâu
4. X-Frame-Options & frame-ancestors
Chống clickjacking — attacker nhúng site bạn vào iframe trong suốt, dụ user click button → thực hiện action.
X-Frame-Options (legacy)
X-Frame-Options: DENY # không cho ai iframe
X-Frame-Options: SAMEORIGIN # chỉ cùng origin iframe được
CSP frame-ancestors (modern)
Content-Security-Policy: frame-ancestors 'none';
Content-Security-Policy: frame-ancestors 'self';
Content-Security-Policy: frame-ancestors https://trusted.example.com;
CSP
frame-ancestorsoverride X-Frame-Options khi cả hai cùng tồn tại (browser hiện đại). Best practice: đặt cả hai cho compat với old browser.
5. X-Content-Type-Options
X-Content-Type-Options: nosniff
Chặn browser "đoán" content type khác với header Content-Type server gửi.
Vì sao quan trọng
Browser cũ có "MIME sniffing": thấy file có header Content-Type: text/plain nhưng nội dung trông như HTML → tự render thành HTML. Attacker upload evil.txt chứa <script> lên CDN → browser render thành HTML → XSS.
nosniff buộc browser respect đúng Content-Type từ server.
Mọi response phải có header này. Không có downside.
6. Referrer-Policy
Kiểm soát header
Referer(tên history sai chính tả) — báo cho browser khi nào gửi URL nguồn cho trang đích.
Vấn đề
URL có thể chứa thông tin nhạy cảm:
https://email.com/inbox?token=xyzhttps://app.com/reset-password?key=abc
Mặc định browser gửi full URL trong Referer khi user click ra ngoài → leak token cho third-party.
Giá trị
| Value | Hành vi |
|---|---|
no-referrer | Không bao giờ gửi |
same-origin | Chỉ gửi same-origin |
strict-origin | Origin only, không gửi nếu downgrade (HTTPS→HTTP) |
strict-origin-when-cross-origin ⭐ | Full URL same-origin, origin-only cross-origin, không gửi downgrade |
unsafe-url | Luôn gửi full URL (NGUY HIỂM) |
Khuyến nghị: strict-origin-when-cross-origin (default mới của Chrome).
7. Permissions-Policy
Bật/tắt browser API mạnh (camera, mic, geolocation, USB, payment...).
Permissions-Policy: camera=(), microphone=(), geolocation=(self), payment=()
Cú pháp:
feature=()— disable hoàn toànfeature=(self)— chỉ origin chínhfeature=(self "https://trusted.com")— origin chính + whitelistfeature=*— cho phép mọi origin (KHÔNG nên)
Các feature thường disable
| Feature | Khi nào tắt |
|---|---|
camera | App không dùng webcam |
microphone | App không dùng mic |
geolocation | App không cần location |
payment | Không có Payment Request API |
usb, serial, hid | Web app thông thường |
interest-cohort | Opt-out FLoC (Google ad tracking) |
Càng disable nhiều càng tốt — nếu attacker chèn được XSS, họ không thể access camera/mic/location.
8. Content-Security-Policy đi sâu
CSP là header phức tạp nhất nhưng quan trọng nhất chống XSS. Cấu hình đúng = giảm 80% impact XSS.
Các directive chính
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-r4nd0m';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
report-uri /csp-report;
| Directive | Kiểm soát |
|---|---|
default-src | Fallback cho directive không khai báo |
script-src | Nguồn JavaScript |
style-src | Nguồn CSS |
img-src | Nguồn hình ảnh |
font-src | Nguồn font |
connect-src | fetch, XMLHttpRequest, WebSocket |
frame-src | Nguồn <iframe> được embed |
frame-ancestors | Site nào được iframe trang này |
base-uri | <base href> cho phép |
form-action | URL của action trên form |
report-uri / report-to | Endpoint nhận violation report |
Nguồn cho phép
'self' same origin
'none' không nguồn nào
https://cdn.example.com domain whitelist
*.example.com wildcard subdomain (tránh wildcard top-level)
data: data URI (thận trọng với img)
blob: blob URI (cần cho file upload preview)
'unsafe-inline' inline script/style — TRÁNH
'unsafe-eval' eval, new Function() — TRÁNH
'nonce-r4nd0m' script có nonce attribute trùng
'sha256-abc...' script có hash trùng
'strict-dynamic' trust script được load bởi trusted script
Nonce-based CSP — recommended
// Express middleware
const crypto = require('crypto');
app.use((req, res, next) => {
res.locals.cspNonce = crypto.randomBytes(16).toString('base64');
res.setHeader(
'Content-Security-Policy',
`default-src 'self'; script-src 'self' 'nonce-${res.locals.cspNonce}'; object-src 'none'; base-uri 'self';`
);
next();
});
// Template
// <script nonce="<%= cspNonce %>">console.log('OK')</script>
Nonce phải:
- Random, cryptographically secure (≥128 bit)
- Khác nhau mỗi request
- Không gửi qua URL hoặc lưu cache
Hash-based CSP — cho inline script tĩnh
Content-Security-Policy: script-src 'self' 'sha256-RFWPLDbv2BY+rCkDzsE+0fr8ylGr2R2faWMhq4lfEQc='
<script>console.log('hello');</script>
Browser tính SHA-256 của nội dung <script> (giữa tag mở/đóng), so với policy. Nếu match → execute.
Useful cho: static inline script không thay đổi giữa các request.
strict-dynamic
Content-Security-Policy: script-src 'nonce-r4nd0m' 'strict-dynamic';
Cho phép script có nonce load thêm script không cần nonce (qua createElement('script')). Useful khi bundle tự load chunk:
// loader.js (có nonce) — được trust
const s = document.createElement('script');
s.src = '/chunks/abc.js'; // chunk không cần nonce
document.head.appendChild(s);
Strict-dynamic là cách hiện đại để CSP chống XSS mà không cần liệt kê whitelist domain (whitelist domain thường bị bypass qua JSONP, AngularJS Sandbox bypass).
Report-Only Mode
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-violations;
Browser không enforce — chỉ gửi POST tới /csp-violations mỗi khi có violation. Dùng khi:
- Mới triển khai CSP, sợ break site
- Đo lường xem policy strict có cause issue thật không
- Test trên production traffic với dữ liệu thật
Workflow:
- Bật
Report-Onlyvới policy strict - Thu thập violation report 1-2 tuần
- Phân tích → tune policy (whitelist domain thật cần, fix inline script bằng nonce)
- Chuyển sang
Content-Security-Policy(enforce)
Sample report payload
{
"csp-report": {
"document-uri": "https://example.com/page",
"violated-directive": "script-src",
"blocked-uri": "https://evil.com/payload.js",
"line-number": 42,
"source-file": "https://example.com/page"
}
}
9. Subresource Integrity (SRI) — Bảo vệ CDN load
Khi load script/CSS từ CDN (jsdelivr, cdnjs), bạn phụ thuộc vào CDN không bị hack. SRI cho phép pin hash của file — browser verify trước khi execute.
Cú pháp
<script
src="https://cdn.example.com/lib.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous">
</script>
<link
rel="stylesheet"
href="https://cdn.example.com/style.css"
integrity="sha384-..."
crossorigin="anonymous">
Cách tính hash
# Hash file local
openssl dgst -sha384 -binary lib.js | openssl base64 -A
# Output: oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC
# Hoặc dùng online: srihash.org
Behavior
- Browser fetch file → tính hash → so với
integrity - Match → execute
- Mismatch → abort, không execute, console error
Khi nào dùng
- Mọi
<script>/<link>từ CDN ngoài - Loader bundle tự generate SRI hash (webpack, esbuild plugin)
- KHÔNG dùng SRI cho asset same-origin của bạn (vô nghĩa, mỗi deploy phải update)
Lưu ý
crossorigin="anonymous"bắt buộc khi load cross-origin- Nếu CDN thay đổi file → hash mismatch → site broken cho đến khi update integrity. Pin version trong URL:
lib@1.2.3không phảilib@latest.
10. Triển khai trên framework phổ biến
Express + helmet
const helmet = require('helmet');
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`],
styleSrc: ["'self'"],
imgSrc: ["'self'", "data:"],
connectSrc: ["'self'"],
frameAncestors: ["'none'"],
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
}));
helmet mặc định bật: HSTS, X-Frame-Options=DENY, X-Content-Type-Options=nosniff, Referrer-Policy, và CSP cơ bản. Một dòng giải quyết 80% headers.
Next.js (next.config.js)
const securityHeaders = [
{
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains; preload',
},
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()',
},
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-inline'; ..."
// Next.js inline script cần nonce hoặc 'unsafe-inline'
// Xem doc Next.js để config nonce middleware
},
];
module.exports = {
async headers() {
return [{ source: '/(.*)', headers: securityHeaders }];
},
};
nginx
server {
listen 443 ssl http2;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none';" always;
}
alwaysflag bắt buộc — không có nó, header chỉ add cho2xx/3xx, không add cho4xx/5xx.
CloudFront / AWS
CloudFront có Response Headers Policy managed — chọn "SecurityHeadersPolicy" để bật tự động.
11. Đo lường security posture
securityheaders.com
https://securityheaders.com/?q=yoursite.com&hide=on&followRedirects=on
Output:
A+ → tất cả headers tốt
A → thiếu 1-2 header phụ
B/C → thiếu CSP hoặc HSTS
F → không có header nào
Hữu ích cho: nhanh chóng baseline, compare với competitor, theo dõi regression.
Mozilla Observatory
https://observatory.mozilla.org/
Test sâu hơn:
- HTTP → HTTPS redirect
- HSTS preload status
- Cookie security flags
- TLS configuration (linked to SSL Labs)
- Output: điểm 0-130 + grade A-F
SSL Labs (TLS-specific)
https://www.ssllabs.com/ssltest/
Test:
- TLS version supported
- Cipher suites
- Cert chain
- Vulnerabilities (POODLE, BEAST, Heartbleed...)
- Score A+ -> F
Browser DevTools
Network tab → click request → Headers
"Issues" panel (Chrome) báo CSP violation và security issue
12. Câu hỏi ôn tập
-
Tại sao
'unsafe-inline'trongscript-srcgần như vô hiệu hoá tác dụng chống XSS của CSP?Xem đáp án
CSP chống XSS bằng cách chặn execute inline script không được whitelist.
'unsafe-inline'cho phép bất kỳ inline<script>nào execute — nghĩa là nếu attacker chèn được<script>alert(1)</script>qua Stored XSS, CSP không chặn. Toàn bộ lý do dùng CSP gần như mất.Alternative: dùng
'nonce-{random}'(mỗi request một nonce, attacker không biết được) hoặc'sha256-...'(hash của script cố định). Inline script không có nonce/hash đúng sẽ bị block.Thực tế: nhiều site bật
'unsafe-inline'để khỏi refactor inline analytics/ads — kết quả CSP chỉ là "decoration", không có security value thật. -
Khác biệt giữa X-Frame-Options và CSP
frame-ancestors?Xem đáp án
Cùng mục đích (chống clickjacking) nhưng:
- X-Frame-Options: header riêng, chỉ có 3 giá trị (
DENY,SAMEORIGIN,ALLOW-FROM uri—ALLOW-FROMđã deprecated). Một URL whitelist duy nhất. - CSP frame-ancestors: directive của CSP, support nhiều origin whitelist, support source expression (
'self','none', scheme, wildcard).
frame-ancestorsmới hơn, mạnh hơn. Trên browser hiện đại,frame-ancestorsoverride X-Frame-Options. Best practice: đặt cả hai cho IE11/Safari cũ (X-Frame-Options) lẫn modern (frame-ancestors). - X-Frame-Options: header riêng, chỉ có 3 giá trị (
-
App của bạn load Google Fonts từ
fonts.googleapis.com. CSP cần gì để không break?Xem đáp án
Google Fonts load qua 2 host:
fonts.googleapis.com— CSS file (@font-facedeclaration)fonts.gstatic.com— file font thật (.woff2)
CSP cần:
style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com;Nếu thiếu
style-srcchogoogleapis.com→ CSS bị block. Thiếufont-srcchogstatic.com→ font không render. Đây là pattern phổ biến nhất khi bật CSP — phải tune theo CDN/third-party thật site dùng. -
HSTS với
preloadcó rủi ro gì?Xem đáp án
Preload list rất khó remove. Sau khi submit và được accept vào Chrome/Firefox/Safari preload list:
- Update binary browser được vài tháng/quý
- User trên browser cũ có thể giữ entry hàng năm
- Removal request mất 3-12 tháng để propagate
Risk:
- Nếu certificate hết hạn không renew kịp → site đỏ hoàn toàn (không có "Continue anyway" cho HSTS preload)
- Nếu cần tạm thời HTTP (debug, migration) → không thể
- Subdomain mới phải HTTPS từ ngày đầu (do
includeSubDomains)
Best practice: HSTS
max-age=300test 1 tuần → tăng lên2592000(30 ngày) → cuối cùng31536000(1 năm) +preload. Chỉ submit preload khi infra ổn định. -
SRI integrity hash bảo vệ trước scenario nào? Khi nào SRI là vô nghĩa?
Xem đáp án
SRI bảo vệ trước:
- CDN bị hack → attacker thay nội dung file → browser tính hash khác → block execute
- Supply chain attack: maintainer của lib push malicious update → hash cũ không match → block
- MITM trên HTTP (dù HTTPS đã cover phần lớn, SRI là double check)
SRI vô nghĩa khi:
- Asset same-origin của chính bạn — bạn control server, SRI chỉ thêm pain (mỗi deploy phải update hash)
- URL với
@latesttag — file thay đổi liên tục, SRI sẽ break thường xuyên - Khi bạn cần feature mới của lib và auto-update — SRI ngăn cập nhật silent
SRI là tradeoff: an toàn hơn vs flexibility ít hơn. Khuyến nghị: pin version
lib@1.2.3+ SRI hash cho mọi CDN third-party.
Bài tập thực hành
# 1. Test security headers của site bạn
curl -I https://yoursite.com | grep -iE "strict-transport|content-security|x-frame|x-content-type|referrer|permissions"
# 2. Đo điểm với securityheaders.com
open "https://securityheaders.com/?q=yoursite.com&followRedirects=on"
# 3. Tính SRI hash cho file CDN
curl -s https://cdn.example.com/lib.js | \
openssl dgst -sha384 -binary | \
openssl base64 -A
# 4. Bật helmet trong Express app
npm install helmet
# Thêm: app.use(helmet());
# Test lại với curl -I, các header tự xuất hiện
# 5. Triển khai CSP report-only mode
# Bật header Content-Security-Policy-Report-Only
# Endpoint /csp-violations log mọi violation
# Theo dõi 1 tuần, fix violation thật, sau đó enforce
# 6. Hardened TLS check
# Test với SSL Labs:
open "https://www.ssllabs.com/ssltest/analyze.html?d=yoursite.com"
# Mục tiêu A+: TLS 1.2/1.3 only, strong cipher, HSTS preload-ready
Tài liệu tham khảo chính thức
- MDN — HTTP Security Headers
- MDN — Content Security Policy
- OWASP Secure Headers Project
- helmet (Node.js)
- Google CSP Evaluator
- HSTS Preload List
- SRI Hash Generator
Tiếp theo: Ngày 6 — Quiz Tuần 1