XSS xảy ra qua JSON như thế nào
Kết luận: JSON không bao giờ tự thực thi. XSS xảy ra đúng lúc bạn coi nó là HTML — nối chuỗi phía máy chủ, innerHTML phía frontend, hoặc nhúng JSON thẳng vào thẻ script.

Tóm lại: JSON không bao giờ tự thực thi — hãy đóng ba con đường đầu ra trước: escape phía máy chủ theo ngữ cảnh, dùng textContent thay vì innerHTML, và thay < bằng \u003c khi nhúng JSON.
{"name": "<img src=x onerror=alert(1)>"} là dữ liệu hợp lệ. Việc nó có nguy hiểm hay không phụ thuộc hoàn toàn vào nơi bạn đặt nó.
Điểm xâm nhập một: dựng chuỗi phía máy chủ
res.send(`<div>Hello, ${data.name}</div>`);
Trình duyệt phân tích các thẻ bên trong data.name. JSON ở đây chỉ là vật chứa; lỗi là escape theo sai ngữ cảnh.
Cách sửa: dùng escape mặc định của engine template (<%= %> trong EJS, {{ }} trong Vue và Django). Tránh <%- %>, v-html và |safe cho bất cứ thứ gì do người dùng kiểm soát.
Điểm xâm nhập hai: innerHTML
el.innerHTML = user.bio; // parses markup
el.textContent = user.bio; // inserts text
{user.bio} của React và {{ user.bio }} của Vue tạo ra text node và an toàn theo mặc định. dangerouslySetInnerHTML và v-html là những lựa chọn từ bỏ sự an toàn một cách tường minh.
Nếu bạn thực sự cần văn bản giàu định dạng, hãy làm sạch bằng danh sách trắng (DOMPurify và tương tự) thay vì tin tưởng backend.
Điểm xâm nhập ba: nhúng vào thẻ script
<script>
window.__DATA__ = {"name": "x</script><script>alert(1)</script>"};
</script>
Một </script> bên trong dữ liệu đóng khối sớm và mọi thứ sau đó chạy như một script mới. Đây là trường hợp bị bỏ sót nhiều nhất.
JSON.stringify(data)
.replace(/</g, '\\u003c')
.replace(/>/g, '\\u003e')
.replace(/&/g, '\\u0026');
Vẫn là JSON hợp lệ, giống hệt sau khi phân tích, không còn khả năng đóng thẻ. Next.js và Nuxt làm điều tương tự cho __NEXT_DATA__ và __NUXT__.
Hai trường hợp lân cận đáng biết
JSON hijacking — trình duyệt cũ thực thi JSON được nạp qua <script src>; việc định nghĩa lại Array làm rò rỉ nội dung. Trình duyệt hiện đại đã khắc phục, và Content-Type: application/json cùng X-Content-Type-Options: nosniff đóng nốt phần còn lại.
Tàn dư JSONP — một tên callback do người dùng kiểm soát chính là thực thi mã tùy ý theo thiết kế. Đừng dùng nó trong hệ thống mới.
Danh sách kiểm tra
- Mọi nơi trường JSON chạm tới HTML đều được escape theo ngữ cảnh (HTML, thuộc tính, JS, URL)
- Không có
innerHTML,v-htmlhaydangerouslySetInnerHTMLnào tiêu thụ chuỗi từ API - JSON nhúng tại chỗ escape
<,>và& - Văn bản giàu định dạng đi qua làm sạch bằng danh sách trắng, không phải lọc danh sách đen
- Phản hồi mang
Content-Type: application/jsonvànosniff
Câu hỏi thường gặp
Vì sao JSON hợp lệ vẫn có thể kích hoạt XSS?
JSON chỉ là văn bản và không bao giờ thực thi; việc nó hợp lệ chỉ nghĩa là nó phân tích được. Nguy hiểm xuất hiện khi nó được ghi vào ngữ cảnh HTML hoặc script: trình duyệt phân tích các thẻ, hoặc một </script> đóng khối sớm và phần còn lại chạy như mã. Lỗi nằm ở việc thiếu escape theo ngữ cảnh tại đầu ra, không phải ở định dạng dữ liệu.
Tôi nên dùng textContent hay innerHTML để hiển thị trường từ API?
Ưu tiên textContent: nó chèn giá trị như văn bản thuần, nên các thẻ được hiển thị nguyên văn. innerHTML phân tích markup, nên một trường từ API chứa <img onerror=...> sẽ thực thi. {user.bio} của React và {{ user.bio }} của Vue mặc định tạo text node; dangerouslySetInnerHTML và v-html là những lựa chọn từ bỏ an toàn một cách tường minh.
Tôi nên escape JSON nhúng trong thẻ <script> thế nào?
Vâng, nên làm. Một </script> bên trong dữ liệu đóng khối sớm và mọi thứ sau đó chạy như một script mới. Sau khi tuần tự hóa, hãy thay <, > và & bằng \u003c, \u003e và \u0026: kết quả vẫn là JSON hợp lệ, phân tích giống hệt, và không còn có thể đóng thẻ.