__proto__ injection คืออะไร
สรุปสำคัญ: JSON.parse มอง __proto__ เป็นคีย์ธรรมดา; ความเสียหายเกิดใน recursive merge ที่เขียนไปยัง Object.prototype ทำให้ทุก object สืบทอดพร็อพเพอร์ตี้ของผู้โจมตี

สรุปสั้น ๆ: บล็อก __proto__, constructor และ prototype ก่อนการ merge ใด ๆ, merge ข้อมูลที่ไม่น่าเชื่อถือลงใน Object.create(null) และเพิ่ม assertion เรื่อง prototype ใน CI; การแพตช์ไลบรารีอย่างเดียวไม่พอ
พูดสั้น ๆ: คีย์ที่ไม่น่าเชื่อถือชื่อ __proto__ เจอ recursive merge และ merge นั้นเขียนไปยัง prototype
การจำลอง
function merge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object' && source[key] !== null) {
target[key] = target[key] || {};
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
merge({}, JSON.parse('{"__proto__": {"isAdmin": true}}'));
console.log({}.isAdmin); // true
ไม่มี object ทางธุรกิจใดถูกแตะต้อง Object.prototype ต่างหาก และตอนนี้ทุก object พก isAdmin ไปด้วย
ทำไม JSON.parse ไม่ใช่ต้นเหตุ
const data = JSON.parse('{"__proto__": {"a": 1}}');
Object.getOwnPropertyNames(data); // [ '__proto__' ]
Object.getPrototypeOf(data) === Object.prototype; // true, untouched
__proto__ มาถึงเป็น own property ธรรมดา อันตรายอยู่ที่โค้ดซึ่งภายหลัง merge, deep copy หรือ set ตาม path ทั้งหมด — _.merge, รูปแบบ Object.assign บางแบบ และตัวช่วย set(obj, path, value) ที่เขียนเองนับไม่ถ้วน
ผลกระทบ
- ข้ามการอนุญาต:
if (user.isAdmin)กลายเป็นจริงโดยไม่มีเงื่อนไข - ข้อมูลเสียหาย: ฟิลด์ที่ไม่คาดคิดถูก serialize ลงใน record ที่เก็บไว้
- Denial of service: object ขนาดใหญ่ที่ติดกับ prototype ทำให้หน่วยความจำโตถาวร
- ยกระดับสิทธิ์: เมื่อรวมกับ template engine หรือตัวรันคำสั่ง เคยนำไปสู่ remote code execution
การป้องกันสี่ชั้น
กรองคีย์
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
บล็อกแค่ __proto__ ไม่พอ: constructor.prototype ไปถึงที่เดียวกันได้
Merge ลงใน object ที่ไม่มี prototype
const safeTarget = Object.create(null);
เมื่อไม่มี prototype chain __proto__ ก็เป็นแค่คีย์สตริง
ใช้ Object.hasOwn แทน in เพราะ in จะสอบถาม prototype chain
อัปเดต dependency ให้เป็นเวอร์ชันล่าสุด lodash, deepmerge และ json-patch ต่างเคยออกแพตช์สำหรับบั๊กประเภทนี้ ให้ติดตาม npm audit
ตรวจสอบว่ายังปลอดภัย
ป้อน payload ที่มี __proto__ และ constructor ผ่าน merge ของคุณ แล้ว assert:
console.log({}.polluted); // undefined
console.log(Object.prototype.polluted); // undefined
assertion หนึ่งตัวใน CI ต้นทุนเกือบศูนย์ จับ regression จากการ refactor ภายหลังได้
คำถามที่พบบ่อย
การ merge ควรกรองคีย์ใดออก?
บล็อก __proto__, constructor และ prototype พร้อมกัน กรองแค่ __proto__ ไม่พอ เพราะ constructor.prototype ไปถึง prototype เดียวกันได้
ทำไม Object.create(null) หยุดการเขียนได้?
object ที่ไม่มี prototype มี prototype chain ว่าง __proto__ จึงยังเป็นคีย์สตริงธรรมดา มันใช้เป็นที่เก็บข้อมูลได้ แต่ไม่ทดแทนการกรองคีย์หรือการอัปเดต dependency
จะทดสอบ prototype pollution ใน CI ได้อย่างไร?
ส่ง payload ที่มี __proto__ และ constructor ผ่าน merge ของคุณ แล้ว assert ว่า {}.polluted และ Object.prototype.polluted เป็น undefined assertion หนึ่งตัวต้นทุนแทบไม่มี และจับ regression จากการ refactor ภายหลังได้