Lê Ngọc Tú← Về trang chủ

Đăng ngày 10/11/2026

Header bảo mật cho portfolio tĩnh: CSP, HSTS và lần thử bỏ unsafe-inline thất bại

Kinh nghiệm cấu hình sáu header bảo mật cho trang portfolio tĩnh Next.js trên Vercel, lần thử experimental.sri thất bại và những đánh đổi thực tế.

  • Bảo mật
  • Next.js
  • CSP
  • HSTS
Header bảo mật cho portfolio tĩnh: CSP, HSTS và lần thử bỏ unsafe-inline thất bại

Ngày 31/10/2026, tại commit 7d0e83b (09:16), mình thêm sáu header bảo mật cho trang portfolio tĩnh Next.js 16.4.3 trên Vercel tại www.lengoctu.com, tắt X-Powered-By (poweredByHeader: false) và thoát < trong JSON-LD. Commit này gộp lộ trình thành tựu, bỏ mã thừa và SEO, nên không tách riêng được bảo mật. Trang không gọi API ngoài; mọi script, ảnh, font, media đều cùng nguồn.

Sáu header đang áp dụng

Cấu hình trong next.config.ts gồm sáu header:

HeaderGiá trị
Content-Security-Policydefault-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; media-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests
X-Content-Type-Optionsnosniff
X-Frame-OptionsDENY
Referrer-Policystrict-origin-when-cross-origin
Permissions-Policycamera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()
Strict-Transport-Securitymax-age=63072000; includeSubDomains

X-Frame-Options: DENY và frame-ancestors 'none' của CSP cùng tồn tại song song; repo không ghi lý do giữ cả hai. Khi dev, script-src thêm 'unsafe-eval'.

Đánh đổi khi giữ unsafe-inline cho script

Trong next.config.ts, mình ghi chú: trang tĩnh nên giữ 'unsafe-inline' cho script và style; dùng CSP nonce buộc render theo từng yêu cầu.

Tài liệu Next.js 16.4.3 (guide content-security-policy) nêu: nonce sinh mới mỗi lượt xem nên phải dùng dynamic rendering; trang tĩnh lúc build không có request để chèn nonce. Tài liệu cũng giới thiệu experimental.sri (thử nghiệm): sinh hash tệp JavaScript lúc build, thêm integrity vào thẻ script để giữ trang tĩnh.

Lần thử bỏ unsafe-inline bằng experimental.sri thất bại

Theo ghi chú cấu hình và commit 87d4cb6 (31/10/2026, 22:09), mình từng thử bỏ 'unsafe-inline' ở script bằng experimental.sri. Kết quả là script nội tuyến của Next bị chặn và trang không hydrate. Ghi chú trong mã giải thích rằng script nội tuyến của Next đổi theo từng bản build.

Chưa lưu lại thông báo lỗi trình duyệt và chưa đo lại bằng số liệu. Kết luận, với cấu hình này (Next.js 16.4.3 và experimental.sri): bỏ 'unsafe-inline' làm trang hỏng, nên mình giữ lại. Mình giữ 'unsafe-inline' cho script-src và style-src, ghi đánh đổi vào chú thích cấu hình.

HSTS: thêm preload rồi bỏ sau 1 phút 40 giây

Lúc 22:09:09 ngày 31/10/2026, commit 87d4cb6 thêm preload vào HSTS và upgrade-insecure-requests vào CSP "để mọi tài nguyên chạy HTTPS".

Đến 22:10:49 cùng ngày (sau 1 phút 40 giây), commit a65d1cf bỏ preload. Thông điệp commit ghi: giữ max-age=63072000; includeSubDomains, không đăng ký HSTS preload list. Lý do bỏ preload không được ghi lại trong commit; bài này không suy đoán lý do. Giá trị hiện tại trên trang thật là max-age=63072000; includeSubDomains.

Thoát ký tự trong JSON-LD

Dữ liệu JSON-LD chèn vào thẻ <script type="application/ld+json"> qua dangerouslySetInnerHTML bằng JSON.stringify(...).replace(/</g, '\\u003c') ở 4 vị trí: src/app/layout.tsx, src/app/blog/[slug]/page.tsx, src/app/du-an/[slug]/page.tsx và src/app/mo-hinh/[slug]/page.tsx.

Giải thích kỹ thuật: chuỗi JSON trong thẻ script nếu chứa </script> sẽ khiến trình phân tích HTML đóng thẻ sớm; thay < bằng \u003c giữ nguyên nghĩa JSON nhưng tránh đóng thẻ sớm. Dữ liệu này do mình tự viết; chưa có thử nghiệm tấn công nào.

DevToolsGuard: lớp cản trở, không phải bảo mật

Commit 87d4cb6 thêm src/components/security/DevToolsGuard.tsx ở production để chặn F12, Ctrl/Cmd+Shift+I/J/C, Ctrl/Cmd+U và chuột phải.

Chú thích trong mã nêu rõ: đây là lớp cản trở người dùng thường, không phải bảo mật thực sự, vì mã client luôn đọc được qua menu trình duyệt hoặc công cụ khác. Mình chưa đo ảnh hưởng của component này đến người dùng thật (như người cần chuột phải hay công cụ hỗ trợ).

Lần đo trên site thật ngày 10/11/2026

Ngày 10/11/2026, mình dùng curl kiểm tra site thật:

  • curl -I https://www.lengoctu.com/ trả 200 OK với đủ sáu header khớp next.config.ts, không có X-Powered-By, Server: Vercel.
  • curl -I http://www.lengoctu.com/ trả 308 Permanent Redirect về https://www.lengoctu.com/.
  • Tệp HTML trang chủ nặng 148.807 byte, gồm 14 thẻ <script>, trong đó 5 thẻ không có src (nội tuyến). Một thẻ là JSON-LD (type="application/ld+json"), các thẻ còn lại do Next.js sinh ra, chưa phân tích từng thẻ.
  • Địa chỉ http(s) trong HTML gồm www.w3.org, schema.org (khai báo namespace và schema), cùng www.lengoctu.com, huggingface.co, github.com, www.linkedin.com, www.facebook.com, zalo.me (liên kết bấm vào, không phải tài nguyên tải tự động).

Hạn chế cần nói rõ

Nếu bạn cũng đang cấu hình CSP cho trang tĩnh Next.js trên Vercel, mình rất muốn nghe cách bạn xử lý unsafe-inline.

Tác giả

Lê Ngọc Tú, AI Engineer. Viết lại những gì mình đã làm với RAG, AI Agent và mô hình tiếng Việt.

Muốn trao đổi về bài viết này?

Nếu bạn có câu hỏi hoặc góp ý, hãy liên hệ trực tiếp.