JSON Toolkit

__proto__ injection là gì

Kết luận: JSON.parse coi __proto__ là một khóa bình thường; thiệt hại xảy ra trong một phép gộp đệ quy ghi vào Object.prototype, khiến mọi đối tượng kế thừa thuộc tính của kẻ tấn công.

Nâng caoĐọc khoảng 5 phútXuất bản 2026-10-30Cập nhật 2026-10-30
Ô nhiễm prototype hoạt động thế nào và cách chặn

Tóm lại: hãy chặn __proto__, constructor và prototype trước mọi phép gộp, gộp dữ liệu không tin cậy vào Object.create(null), và thêm một phép khẳng định về prototype vào CI; chỉ vá thư viện là chưa đủ.

Nói một câu: một khóa không tin cậy tên __proto__ gặp một phép gộp đệ quy, và phép gộp ghi vào prototype.

Tái hiện

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

Không đối tượng nghiệp vụ nào bị chạm tới. Object.prototype thì có, và mọi đối tượng giờ đều mang isAdmin.

Vì sao JSON.parse không có lỗi

const data = JSON.parse('{"__proto__": {"a": 1}}');
Object.getOwnPropertyNames(data);                  // [ '__proto__' ]
Object.getPrototypeOf(data) === Object.prototype;  // true, untouched

__proto__ đến như một thuộc tính riêng bình thường. Nguy hiểm nằm hoàn toàn ở đoạn mã sau đó gộp, sao chép sâu hoặc gán theo đường dẫn — _.merge, một số mẫu Object.assign, và vô số hàm set(obj, path, value) viết tay.

Tác động

  • Vượt qua phân quyền: if (user.isAdmin) trở thành luôn đúng
  • Hỏng dữ liệu: các trường ngoài dự kiến bị tuần tự hóa vào bản ghi đã lưu
  • Từ chối dịch vụ: các đối tượng lớn gắn vào prototype làm bộ nhớ phình vĩnh viễn
  • Leo thang: kết hợp với engine template hoặc trình chạy lệnh, nó đã từng dẫn tới thực thi mã từ xa

Bốn lớp phòng thủ

Lọc khóa.

const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);

Chỉ chặn __proto__ là chưa đủ: constructor.prototype cũng đến được cùng một chỗ.

Gộp vào một đối tượng có prototype null.

const safeTarget = Object.create(null);

Không có chuỗi prototype, thì __proto__ chỉ là một khóa chuỗi.

Dùng Object.hasOwn thay vì in. Toán tử in có tra cứu chuỗi prototype.

Luôn cập nhật các dependency. lodash, deepmerge và json-patch đều đã phát hành bản vá cho nhóm lỗi này. Hãy theo dõi npm audit.

Kiểm tra rằng nó vẫn được vá

Hãy đưa một payload chứa __proto__ và constructor qua hàm gộp của bạn, rồi khẳng định:

console.log({}.polluted);                 // undefined
console.log(Object.prototype.polluted);   // undefined

Một phép khẳng định trong CI, chi phí gần như bằng không, bắt được thoái hóa từ những lần tái cấu trúc sau này.

Câu hỏi thường gặp

Phép gộp nên lọc bỏ những khóa nào?

Chặn __proto__, constructor và prototype cùng lúc. Chỉ lọc __proto__ là chưa đủ, vì constructor.prototype cũng đến được cùng một prototype.

Vì sao Object.create(null) chặn được việc ghi?

Đối tượng có prototype null không có chuỗi prototype, nên __proto__ vẫn chỉ là khóa chuỗi thông thường. Nó dùng được làm vật chứa dữ liệu, nhưng không thay thế việc lọc khóa hay cập nhật dependency.

Làm sao kiểm tra prototype pollution trong CI?

Gửi một payload chứa __proto__ và constructor qua hàm gộp của bạn, rồi khẳng định rằng {}.polluted và Object.prototype.polluted là undefined. Một phép khẳng định gần như không tốn gì và bắt được thoái hóa từ những lần tái cấu trúc sau này.

Đọc thêm