XSS เกิดขึ้นผ่าน JSON ได้อย่างไร
สรุปสำคัญ: JSON ไม่เคยทำงานเอง XSS เกิดขึ้นในจังหวะที่คุณปฏิบัติกับมันเป็น HTML — การต่อสตริงฝั่งเซิร์ฟเวอร์, innerHTML ฝั่ง frontend หรือการฝัง JSON ลงในแท็ก script โดยตรง

สรุปสั้น ๆ: JSON ไม่เคยทำงานเอง — ปิดเส้นทาง output สามทางก่อน: escape ฝั่งเซิร์ฟเวอร์ตามบริบท, ใช้ textContent แทน innerHTML และแทน < ด้วย \u003c เมื่อฝัง JSON
{"name": "<img src=x onerror=alert(1)>"} เป็นข้อมูลที่ถูกต้อง มันอันตรายหรือไม่ขึ้นกับว่าคุณวางมันไว้ที่ไหนทั้งหมด
ทางที่หนึ่ง: การต่อสตริงฝั่งเซิร์ฟเวอร์
res.send(`<div>Hello, ${data.name}</div>`);
เบราว์เซอร์ parse แท็กภายใน data.name JSON ที่นี่เป็นแค่ภาชนะ; ความผิดคือการ escape ผิดบริบท
วิธีแก้: ใช้การ escape โดยค่าเริ่มต้นของ template engine (<%= %> ใน EJS, {{ }} ใน Vue และ Django) เลี่ยง <%- %>, v-html และ |safe สำหรับอะไรก็ตามที่ผู้ใช้ควบคุม
ทางที่สอง: innerHTML
el.innerHTML = user.bio; // parses markup
el.textContent = user.bio; // inserts text
{user.bio} ของ React และ {{ user.bio }} ของ Vue สร้าง text node และปลอดภัยโดยค่าเริ่มต้น dangerouslySetInnerHTML และ v-html คือการเลือกปิดความปลอดภัยอย่างชัดเจน
ถ้าต้องใช้ rich text จริง ๆ ให้ sanitize ด้วย allowlist (DOMPurify และอื่น ๆ ที่คล้ายกัน) แทนการเชื่อ backend
ทางที่สาม: ฝังลงในแท็ก script
<script>
window.__DATA__ = {"name": "x</script><script>alert(1)</script>"};
</script>
</script> ภายในข้อมูลปิดบล็อกก่อนเวลา และทุกอย่างหลังจากนั้นทำงานเป็น script ใหม่ นี่คือกรณีที่ถูกละเลยมากที่สุด
JSON.stringify(data)
.replace(/</g, '\\u003c')
.replace(/>/g, '\\u003e')
.replace(/&/g, '\\u0026');
ยังเป็น JSON ที่ถูกต้อง เหมือนเดิมหลัง parse และไม่สามารถปิดแท็กได้อีก Next.js และ Nuxt ทำแบบเดียวกันกับ __NEXT_DATA__ และ __NUXT__
เพื่อนบ้านสองอย่างที่ควรรู้
JSON hijacking — เบราว์เซอร์เก่ารัน JSON ที่โหลดผ่าน <script src>; การนิยาม Array ใหม่ทำให้เนื้อหารั่วไหล เบราว์เซอร์สมัยใหม่แก้แล้ว และ Content-Type: application/json พร้อม X-Content-Type-Options: nosniff ปิดส่วนที่เหลือ
เศษของ JSONP — ชื่อ callback ที่ผู้ใช้ควบคุมคือการรันโค้ดตามอำเภอใจโดยการออกแบบ อย่าใช้ในระบบใหม่
เช็กลิสต์
- ทุกที่ที่ฟิลด์ JSON ไปถึง HTML ถูก escape ตามบริบท (HTML, attribute, JS, URL)
- ไม่มี
innerHTML,v-htmlหรือdangerouslySetInnerHTMLที่รับสตริงจาก API - JSON ที่ฝังไว้ escape
<,>และ& - Rich text ผ่านการ sanitize แบบ allowlist ไม่ใช่การกรองแบบ blacklist
- Response มี
Content-Type: application/jsonและnosniff
คำถามที่พบบ่อย
ทำไม JSON ที่ถูกต้องยังทำให้เกิด XSS ได้?
JSON เป็นเพียงข้อความและไม่เคยทำงาน; การที่มันถูกต้องหมายถึงแค่ว่า parse ได้ อันตรายปรากฏเมื่อมันถูกเขียนลงในบริบท HTML หรือ script: เบราว์เซอร์ parse แท็ก หรือ </script> ปิดบล็อกก่อนเวลาและส่วนที่เหลือทำงานเป็นโค้ด ความผิดคือการขาดการ escape ตามบริบทตอน output ไม่ใช่รูปแบบข้อมูล
ควรใช้ textContent หรือ innerHTML เพื่อแสดงฟิลด์จาก API?
เลือก textContent: มันแทรกค่าเป็นข้อความล้วน แท็กจึงแสดงตามตัวอักษร innerHTML parse markup ฟิลด์จาก API ที่มี <img onerror=...> จึงทำงาน {user.bio} ของ React และ {{ user.bio }} ของ Vue สร้าง text node โดยค่าเริ่มต้น; dangerouslySetInnerHTML และ v-html เป็นการเลือกปิดความปลอดภัยอย่างชัดเจน
ควร escape JSON ที่ฝังในแท็ก <script> อย่างไร?
ใช่ </script> ภายในข้อมูลปิดบล็อกก่อนเวลาและทุกอย่างหลังจากนั้นทำงานเป็น script ใหม่ หลังจาก serialize แล้ว ให้แทน <, > และ & ด้วย \u003c, \u003e และ \u0026: ผลลัพธ์ยังเป็น JSON ที่ถูกต้อง parse เหมือนเดิม และไม่สามารถปิดแท็กได้อีก