Content Security Policy (CSP) cho WordPress nên được rollout như một change có kiểm soát, không phải copy một header “cứng” rồi bật ngay trên production. Mục tiêu của CSP là giới hạn nguồn tài nguyên mà browser được phép tải/thực thi và giảm impact của XSS/content injection. Nhưng WordPress thường có inline script/style, page builder, plugin frontend, analytics, font, CDN, iframe và third-party integration; policy quá chặt có thể làm hỏng login, checkout, form, media hoặc admin flow.
Quy trình tốt là inventory → Report-Only → phân loại violation → refactor/nonce/hash → pilot enforcement → full enforcement → monitoring + rollback.
CSP kiểm soát những lớp nào?
| Directive | Vai trò | Điểm cần lưu ý |
|---|---|---|
default-src |
Fallback cho nhiều fetch directive | Không thay mọi directive; một số directive như frame-ancestors không fallback từ đây |
script-src |
Kiểm soát JavaScript | Strict CSP thường dùng nonce/hash |
style-src |
Kiểm soát CSS | WordPress/plugin có thể dùng inline style |
img-src |
Kiểm soát image source | Cần xét CDN/data URI nếu thực sự dùng |
font-src |
Kiểm soát font source | Hay bị bỏ sót khi dùng font ngoài |
connect-src |
XHR/fetch/WebSocket/beacon | Ảnh hưởng API, analytics, realtime |
frame-src |
Page được phép load iframe từ đâu | Khác với frame-ancestors |
frame-ancestors |
Ai được phép embed trang của bạn | Defense against clickjacking |
base-uri |
Giới hạn <base> |
Hữu ích trong strict CSP |
object-src |
Kiểm soát object/embed | Strict policy thường đặt 'none' |
Đừng bắt đầu bằng allowlist domain khổng lồ
Policy dựa chủ yếu vào danh sách host thường phình to khi site dùng nhiều vendor và dễ vô tình cho phép domain rộng hơn cần thiết. Với script/style quan trọng, nonce hoặc hash giúp ràng buộc execution chặt hơn. Tuy nhiên nonce phải được tạo mới cho mỗi response và không đoán được; đây là thay đổi kiến trúc chứ không chỉ chỉnh header.
WordPress/plugin ecosystem có thể khiến strict CSP phức tạp hơn. Vì vậy cần inventory owner và refactor dần, thay vì thêm 'unsafe-inline' hoặc wildcard mỗi khi gặp violation.
Inventory trước rollout
| Resource | Owner | Source | Route sử dụng | Action |
|---|---|---|---|---|
| Core/theme JS | Theme/core | self | Frontend | Allow / nonce/hash nếu cần |
| Plugin JS/CSS | Plugin | self/CDN | Frontend/admin | Verify owner + route |
| Analytics/tag manager | Marketing/Security | Third-party | Selected routes | Need + data flow review |
| Payment/SSO iframe | Business integration | Third-party | Checkout/login | Frame/connect/form dependencies |
| Fonts/images | Theme/design | self/CDN | Frontend | Allow minimal source |
Nếu resource không có owner hoặc business reason, trạng thái nên là INVESTIGATE, không phải auto-allow.
Stage 1 — Report-Only trước enforcement
Dùng Content-Security-Policy-Report-Only để browser báo violation nhưng chưa block resource. Đây là phase để tìm dependency thật và false positive trước khi enforcement.
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' 'nonce-<dynamic-value>';
object-src 'none';
base-uri 'none';
frame-ancestors 'self';
report-to csp-endpoint;
report-uri /csp-report
Header trên chỉ là cấu trúc minh họa. Không copy nguyên vào production nếu site chưa có nonce generation, reporting endpoint và dependency inventory.
Violation Triage Matrix
| Violation | Owner | Legitimate? | Decision |
|---|---|---|---|
script-src-elem blocked third-party |
Unknown | Unknown | Investigate, không auto-allow |
| Inline script bị block | Theme/plugin | Có thể | Refactor hoặc nonce/hash |
| Connect tới API vendor | Integration | Nếu flow cần | Allow minimal origin |
| Frame bị block | Payment/SSO/widget | Tùy route | Review frame-src / vendor flow |
| Violation từ injected script lạ | Không owner | Không | Security investigation, không nới policy |
Violation Report Record mẫu
report_id: CSP-YYYY-NNN
policy_version: v1-report-only
document_uri: ...
violated_directive: script-src-elem
blocked_uri: ...
source_file: ...
line_number: ...
route: frontend|login|admin|checkout
owner: core|theme|plugin|tag-manager|vendor|unknown
legitimate: yes|no|unknown
decision: allow|minimize|nonce|hash|remove|investigate
change_ref: ...
validation_ref: ...
Nonce và hash: dùng khi nào?
Nonce phù hợp khi server có thể sinh giá trị ngẫu nhiên mới cho từng response và gắn cùng nonce vào các script/style được phép. Hash phù hợp hơn với nội dung tĩnh có thể fingerprint ổn định. Không reuse nonce cố định giữa các response vì làm mất mục tiêu bảo mật.
Trong WordPress, nonce CSP khác với WordPress nonce dùng cho CSRF/action validation; không nên nhầm hai khái niệm.
frame-ancestors và clickjacking
frame-ancestors xác định origin nào được phép embed page của bạn. Nếu site không cần bị embed, policy chặt có thể dùng 'none'. Nếu có portal/payment/support integration, chỉ allow đúng parent origin cần thiết.
frame-ancestors khác frame-src: một cái kiểm parent được embed bạn, cái kia kiểm page của bạn load iframe từ đâu.
Stage 2 — Pilot enforcement
Sau Report-Only, đừng chuyển toàn site sang enforcement một lần nếu blast radius lớn. Chọn route/user flow đại diện và có rollback:
- Frontend public route.
- Login/logout/password reset.
- Admin/editor flow cần thiết.
- Form/contact.
- Checkout/payment/SSO nếu có.
- Media/embed/page builder route.
Trong pilot, giữ Report-Only policy mới song song với enforced policy hiện tại nếu cần test revision tiếp theo.
Acceptance Test Matrix
| Flow | PASS | FAIL |
|---|---|---|
| Frontend render | JS/CSS/media cần thiết hoạt động | Layout/function chính bị block |
| Login/admin | Auth và admin action hoạt động | Login/editor/plugin flow lỗi |
| Form/checkout | Submit/payment callback hoạt động | Connect/frame/form endpoint bị block |
| Security | Injected/unapproved test resource bị CSP chặn hoặc report đúng | Policy phải nới rộng mới “hết lỗi” |
| Monitoring | Violation report tới đúng endpoint | Report pipeline mất dữ liệu |
Rollback Plan
Trước enforcement, lưu policy/version hiện tại và điểm cấu hình tạo header: web server, CDN, reverse proxy, security plugin hoặc application. Nếu business flow critical fail:
- Quay về policy version trước đã PASS.
- Không xóa toàn bộ CSP trừ khi rollback plan đã xác định như vậy.
- Ghi route/directive/resource gây fail.
- Chuyển revision mới về Report-Only.
- Fix owner/dependency rồi pilot lại.
Không dùng CSP để che lỗ hổng ứng dụng
CSP là defense-in-depth, không thay sanitize/escape output, permission check, input validation, patch plugin/theme hoặc incident remediation. Nếu CSP report phát hiện injected script lạ, đó là tín hiệu cần điều tra source chèn code, không phải lý do thêm domain vào allowlist.
Liên kết với security headers và HTTPS
CSP là một phần của browser security controls. Xem Security headers cho WordPress để phối hợp với HSTS, nosniff và các header khác. Trước khi rollout HSTS hoặc CSP yêu cầu HTTPS-only resource, đối chiếu HTTPS/TLS WordPress an toàn.
PASS/FAIL cho rollout CSP
- PASS: dependency inventory có owner; Report-Only đã chạy đủ route; violation được phân loại; pilot flow PASS; policy version + rollback rõ; reporting hoạt động.
- FAIL: bật enforcement trực tiếp; dùng wildcard/
'unsafe-inline'rộng chỉ để hết lỗi; không biết owner resource; login/checkout/admin bị phá; hoặc reporting không hoạt động. - HOLD: còn violation chưa biết owner hoặc route critical chưa test. HOLD phải tiếp tục Report-Only/pilot, không được ép sang full enforcement.
Nguồn kỹ thuật
- MDN — Content Security Policy implementation
- MDN — Content Security Policy guide
- OWASP — Content Security Policy Cheat Sheet
Kết luận
CSP WordPress tốt không phải policy “càng chặt càng tốt”, mà là policy đủ chặt, có owner, có evidence và không phá business flow. Rollout đúng là inventory → Report-Only → violation triage → nonce/hash/refactor → pilot enforcement → full enforcement → monitoring + rollback.
