Đă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

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:
| Header | Giá trị |
|---|---|
Content-Security-Policy | default-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-Options | nosniff |
X-Frame-Options | DENY |
Referrer-Policy | strict-origin-when-cross-origin |
Permissions-Policy | camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=() |
Strict-Transport-Security | max-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 OKvới đủ sáu header khớpnext.config.ts, không cóX-Powered-By,Server: Vercel.curl -I http://www.lengoctu.com/trả308 Permanent Redirectvề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ùngwww.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.