</>Học Dev
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."}},{"@type":"Question","name":"Khác biệt giữa X-Frame-Options và CSP frame-ancestors?","acceptedAnswer":{"@type":"Answer","text":"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-ancestors mới hơn, mạnh hơn. Trên browser hiện đại, frame-ancestors override X-Frame-Options. Best practice: đặt cả hai cho IE11/Safari cũ (X-Frame-Options) lẫn modern (frame-ancestors)."}},{"@type":"Question","name":"App của bạn load Google Fonts từ fonts.googleapis.com. CSP cần gì để không break?","acceptedAnswer":{"@type":"Answer","text":"Google Fonts load qua 2 host: - fonts.googleapis.com — CSS file (@font-face declaration) - fonts.gstatic.com — file font thật (.woff2) CSP cần: Nếu thiếu style-src cho googleapis.com → CSS bị block. Thiếu font-src cho gstatic.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."}},{"@type":"Question","name":"HSTS với preload có rủi ro gì?","acceptedAnswer":{"@type":"Answer","text":"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=300 test 1 tuần → tăng lên 2592000 (30 ngày) → cuối cùng 31536000 (1 năm) + preload. Chỉ submit preload khi infra ổn định."}},{"@type":"Question","name":"SRI integrity hash bảo vệ trước scenario nào? Khi nào SRI là vô nghĩa?","acceptedAnswer":{"@type":"Answer","text":"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 @latest tag — 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 học

Tuần 1 - Ngày 5: Security Headers và CSP

Tuần 1 – Ngày 5

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.com và 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:

  • 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
WithoutheadersWithheadersXSSJSexecuteCSPblockinlinescriptMITMtrênHTTPHSTSforceHTTPSClickjackingiframeX-Frame-OptionsdenyMIMEconfusionattackX-Content-Type-OptionsnosniffSensitiveRefererleakReferrer-Policystrict-originMic/cameraabusePermissions-Policydeny

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

HeaderMục đíchGiá trị khuyến nghị
Strict-Transport-Security (HSTS)Force HTTPS, chống SSL strippingmax-age=31536000; includeSubDomains; preload
Content-Security-Policy (CSP)Whitelist nguồn resource, chặn XSSdefault-src 'self'; script-src 'self' 'nonce-...'
X-Frame-OptionsChống clickjackingDENY hoặc SAMEORIGIN
X-Content-Type-OptionsChống MIME sniffingnosniff
Referrer-PolicyKiểm soát Referer leakstrict-origin-when-cross-origin
Permissions-PolicyBật/tắt browser featurecamera=(), microphone=(), geolocation=()
Cross-Origin-Opener-Policy (COOP)Isolation top windowsame-origin
Cross-Origin-Embedder-Policy (COEP)Isolation embed resourcerequire-corp
Cross-Origin-Resource-Policy (CORP)Chống cross-origin readsame-origin
X-XSS-ProtectionDEPRECATEDKhô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=NSố giây browser cache policy (31536000 = 1 năm)
includeSubDomainsÁp dụng cho mọi subdomain
preloadCho 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ôn 1 năm
  • includeSubDomains có 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.

attacker.compage"Clickđnhnquà"UIgiphíatrên<iframeopacity:0src=bank.com>iframetrongsut[Transfer$1000]userthcsclickvàođây

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-ancestors override 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=xyz
  • https://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ị

ValueHành vi
no-referrerKhông bao giờ gửi
same-originChỉ gửi same-origin
strict-originOrigin only, không gửi nếu downgrade (HTTPS→HTTP)
strict-origin-when-cross-originFull URL same-origin, origin-only cross-origin, không gửi downgrade
unsafe-urlLuô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àn
  • feature=(self) — chỉ origin chính
  • feature=(self "https://trusted.com") — origin chính + whitelist
  • feature=* — cho phép mọi origin (KHÔNG nên)

Các feature thường disable

FeatureKhi nào tắt
cameraApp không dùng webcam
microphoneApp không dùng mic
geolocationApp không cần location
paymentKhông có Payment Request API
usb, serial, hidWeb app thông thường
interest-cohortOpt-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;
DirectiveKiểm soát
default-srcFallback cho directive không khai báo
script-srcNguồn JavaScript
style-srcNguồn CSS
img-srcNguồn hình ảnh
font-srcNguồn font
connect-srcfetch, XMLHttpRequest, WebSocket
frame-srcNguồn <iframe> được embed
frame-ancestorsSite nào được iframe trang này
base-uri<base href> cho phép
form-actionURL của action trên form
report-uri / report-toEndpoint 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
// 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:

  1. Bật Report-Only với policy strict
  2. Thu thập violation report 1-2 tuần
  3. Phân tích → tune policy (whitelist domain thật cần, fix inline script bằng nonce)
  4. 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.3 không phải lib@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;
}

always flag bắt buộc — không có nó, header chỉ add cho 2xx/3xx, không add cho 4xx/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

  1. Tại sao 'unsafe-inline' trong script-src gầ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.

  2. 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 uriALLOW-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-ancestors mới hơn, mạnh hơn. Trên browser hiện đại, frame-ancestors override X-Frame-Options. Best practice: đặt cả hai cho IE11/Safari cũ (X-Frame-Options) lẫn modern (frame-ancestors).

  3. 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-face declaration)
    • 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-src cho googleapis.com → CSS bị block. Thiếu font-src cho gstatic.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.

  4. HSTS với preload có 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=300 test 1 tuần → tăng lên 2592000 (30 ngày) → cuối cùng 31536000 (1 năm) + preload. Chỉ submit preload khi infra ổn định.

  5. 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 @latest tag — 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


Tiếp theo: Ngày 6 — Quiz Tuần 1