JSON Toolkit

Czym jest wstrzyknięcie __proto__

Podsumowanie: JSON.parse traktuje __proto__ jako zwykły klucz; szkoda powstaje w rekurencyjnym scalaniu, które zapisuje do Object.prototype, więc każdy obiekt dziedziczy właściwości atakującego.

Zaawansowany~5 min czytaniaOpublikowano 2026-10-30Zaktualizowano 2026-10-30
Jak działa zanieczyszczenie prototypu i jak je zablokować

Najważniejsze: blokuj __proto__, constructor i prototype przed jakimkolwiek scalaniem, scalaj niezaufane dane wejściowe do Object.create(null) i dodaj asercję prototypu do CI; łatowanie bibliotek samo nie wystarczy.

W jednym zdaniu: niezaufany klucz o nazwie __proto__ spotyka rekurencyjne scalanie, a scalanie zapisuje do prototypu.

Reprodukcja

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

Żaden obiekt biznesowy nie został dotknięty. Dotknięto Object.prototype, a każdy obiekt nosi teraz isAdmin.

Dlaczego JSON.parse nie jest winny

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

__proto__ przychodzi jako zwykła właściwość własna. Niebezpieczeństwo tkwi całkowicie w kodzie, który później scala, głęboko kopiuje lub ustawia po ścieżce — _.merge, część wzorców Object.assign i niezliczone ręcznie pisane pomocniki set(obj, path, value).

Wpływ

  • Obejście autoryzacji: if (user.isAdmin) staje się bezwarunkowo prawdziwe
  • Uszkodzenie danych: nieoczekiwane pola serializują się do przechowywanych rekordów
  • Odmowa usługi: duże obiekty przyczepione do prototypu trwale zwiększają pamięć
  • Eskalacja: w połączeniu z silnikami szablonów lub runnerami poleceń prowadziło to do zdalnego wykonania kodu

Cztery obrony

Filtruj klucze.

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

Blokowanie tylko __proto__ jest niewystarczające: constructor.prototype dociera w to samo miejsce.

Scalaj do obiektu bez prototypu.

const safeTarget = Object.create(null);

Bez łańcucha prototypów __proto__ jest zwykłym kluczem łańcuchowym.

Używaj Object.hasOwn zamiast in. in konsultuje łańcuch prototypów.

Utrzymuj zależności na bieżąco. lodash, deepmerge i json-patch wszystkie wypuściły poprawki dla tej klasy błędów. Obserwuj npm audit.

Zweryfikuj, że pozostaje naprawione

Przepuść ładunek zawierający __proto__ i constructor przez swoje scalanie, a potem asercuj:

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

Jedna asercja w CI, niemal zerowy koszt, łapie regresje z późniejszych refaktorów.

FAQ

Które klucze powinno filtrować scalanie?

Blokuj __proto__, constructor i prototype razem. Filtrowanie tylko __proto__ jest niewystarczające, bo constructor.prototype dociera do tego samego prototypu.

Dlaczego Object.create(null) zatrzymuje zapis?

Obiekt bez prototypu nie ma łańcucha prototypów, więc __proto__ pozostaje zwykłym kluczem łańcuchowym. Działa jako kontener danych, ale nie zastępuje filtrowania kluczy ani aktualizacji zależności.

Jak testować zanieczyszczenie prototypu w CI?

Przepuść ładunek zawierający __proto__ i constructor przez swoje scalanie, a potem asercuj, że {}.polluted i Object.prototype.polluted są undefined. Jedna asercja kosztuje niemal nic i łapie regresje z późniejszych refaktorów.

Dodatkowe materiały