Przejdź do treści
INDEKS ANALIZ BREACHROAD / NOTA TECHNICZNA

Prototype pollution w JavaScript: testy i obrona

Prototype pollution w JavaScript i Node.js: zobacz źródła skażenia, groźne gadżety, bezpieczną metodykę testów oraz skuteczny hardening aplikacji.

MATERIAŁ PUBLICZNY
AUTOR
/ Pentester (OSCP, PNPT)
PUBLIKACJA
5 maja 2026
CZAS CZYTANIA
16 min czytania
TEMAT
AppSec
Prototype pollution w JavaScript: testy i obrona

Prototype pollution to podatność charakterystyczna dla ekosystemu JavaScript, w której dane kontrolowane przez użytkownika modyfikują prototyp obiektu. Skażona właściwość może zostać odziedziczona przez wiele pozornie niezależnych obiektów i zmienić konfigurację zapytania, decyzję autoryzacyjną, działanie sanitizera albo parametry funkcji uruchamianej na serwerze.

Najkrótsza odpowiedź: pełny atak ma zwykle dwie fazy. Najpierw potrzebne jest źródło skażenia — kod zapisujący kontrolowany klucz do obiektu lub niebezpiecznie scalający dane. Potem potrzebny jest „gadżet”, czyli fragment aplikacji, który odczyta odziedziczoną właściwość i nada jej znaczenie bezpieczeństwa. Filtrowanie samego ciągu __proto__ nie wystarcza; trzeba walidować schemat, odrzucać nieznane klucze i nie ufać właściwościom odziedziczonym.

MITRE opisuje tę klasę jako CWE-1321: Improperly Controlled Modification of Object Prototype Attributes. MDN udostępnia osobny przewodnik JavaScript prototype pollution, który rozdziela źródła skażenia od celów wykorzystania i wskazuje konkretne mechanizmy obronne.

Dlaczego prototyp zmienia zachowanie wielu obiektów

JavaScript używa łańcucha prototypów. Jeżeli właściwości nie ma bezpośrednio w obiekcie, silnik szuka jej w prototypie, potem w prototypie prototypu, aż do Object.prototype. Ta cecha pozwala współdzielić metody, ale staje się niebezpieczna, gdy aplikacja zakłada, że brak pola oznacza wartość fałszywą lub bezpieczny domyślny stan.

const user = { name: "tester" };

console.log(Object.hasOwn(user, "isAdmin")); // false
console.log(user.isAdmin);                   // undefined

Jeżeli podatny kod dopuści ustawienie Object.prototype.isAdmin = true, odczyt user.isAdmin zwróci wartość odziedziczoną. Nie oznacza to, że każda aplikacja automatycznie przyzna rolę administratora. Musi istnieć gadżet: logika autoryzacji, która sprawdza wartość pola bez potwierdzenia, że pole należy do konkretnego obiektu.

Najważniejsze rozróżnienie brzmi:

ElementPytanie testowePrzykład
sourceczy wejście pozwala modyfikować prototyp?deep merge, parser ścieżek, dynamiczny zapis
propagationgdzie skażona wartość jest widoczna?ten obiekt, lokalny realm, cały proces
gadgetczy kod nadaje właściwości skutek?autoryzacja, fetch, template, proces potomny
impactco realnie może osiągnąć atakujący?XSS, bypass, SSRF, DoS, czasem RCE

Bez gadżetu znalezione źródło jest ważnym błędem integralności, ale nie należy automatycznie deklarować zdalnego wykonania kodu. Dobry raport rozdziela potwierdzone zachowanie od hipotetycznych łańcuchów.

Skąd bierze się skażenie prototypu

Dynamiczne przypisanie klucza

Kod w rodzaju target[key] = value jest ryzykowny, jeżeli key pochodzi z requestu i nie przechodzi allowlisty. Klucz może wskazywać __proto__ albo tworzyć ścieżkę constructor.prototype. Niebezpieczne są zwłaszcza helpery obsługujące notację kropkową, na przykład profile.preferences.theme.

Rekurencyjne scalanie obiektów

Funkcja deep merge zwykle przechodzi po kluczach źródła i kopiuje je do celu. Jeżeli traktuje specjalne nazwy jak zwykłe dane, może zejść do obiektu prototypu. Podatność może znajdować się w kodzie aplikacji, bibliotece narzędziowej, parserze parametrów albo warstwie konfiguracji.

Parsery query string i formularzy

Rozbudowane parsery potrafią zamieniać nazwy pól w zagnieżdżone struktury. Warto sprawdzić, czy blokują klucze specjalne na każdym poziomie, a nie tylko na początku ciągu. Aktualizacja zależności jest istotna, ale zarządzanie podatnościami nie zastąpi przeglądu własnych funkcji transformujących dane.

Łączenie JSON z obiektem konfiguracji

Samo JSON.parse() tworzy zwykłe dane; problem często pojawia się później podczas przypisywania lub scalania. MDN pokazuje, że zachowanie Object.assign() może różnić się od spread syntax w kontekście settera __proto__. Wniosek nie brzmi „spread zawsze jest bezpieczny”, lecz „trzeba rozumieć semantykę każdej transformacji i walidować wejście przed nią”.

Gadżety: moment, w którym skażenie staje się atakiem

Najciekawsze cele to obiekty opcji. Aplikacja przekazuje częściowo zdefiniowaną konfigurację do fetch(), biblioteki szablonów, mechanizmu renderowania, klienta HTTP lub funkcji systemowej. Brakujące wartości są wtedy odczytywane z prototypu.

Typowe klasy gadżetów obejmują:

  • autoryzację — kod zakłada, że brak isAdmin oznacza false;
  • żądania HTTP — odziedziczone method, body, headers lub URL zmieniają połączenie;
  • DOM i templating — skażona konfiguracja dociera do srcdoc, HTML lub niebezpiecznego sinka;
  • sanitizery — domyślna konfiguracja zostaje poszerzona o właściwość zmieniającą listę dozwoloną;
  • procesy serwerowe — opcje funkcji uruchamiającej polecenie lub moduł mogą stać się gadżetem;
  • odmowę usługi — zmiana oczekiwanego typu powoduje wyjątki w wielu requestach.

W OWASP Top 10 dla aplikacji webowych prototype pollution może materializować się jako błąd kontroli dostępu, injection lub problem integralności. Sama etykieta technologiczna nie określa skutku; decyduje przepływ danych.

Metodyka bezpiecznego testu

Test wykonuj w środowisku uzgodnionym z właścicielem. Skażenie globalnego prototypu w długowiecznym procesie Node.js może wpłynąć na requesty innych użytkowników. Na produkcji preferuj nieszkodliwe, unikalne znaczniki, minimalną liczbę prób i natychmiastowy restart procesu, jeżeli zespół uzna go za konieczny.

1. Inwentaryzacja transformacji danych

W przeglądzie kodu szukaj dynamicznych zapisów, funkcji merge/clone/defaults, parserów ścieżek, deserializacji konfiguracji i miejsc kopiujących request do obiektu domenowego. SAST i code review są tu szczególnie wartościowe, bo źródło i gadżet mogą znajdować się daleko od siebie.

2. Potwierdzenie source bez wpływu biznesowego

Użyj niekolizyjnej nazwy właściwości i neutralnej wartości. Sprawdź, czy pojawia się ona jako odziedziczona na nowym, pustym obiekcie albo w konkretnym obiekcie docelowym. Nie ustawiaj pól takich jak isAdmin, shell, method czy ścieżek wykonawczych, dopóki zakres nie przewiduje takiej eskalacji.

3. Ustalenie zasięgu

Czy efekt dotyczy jednego obiektu, jednego requestu, workera, całego procesu czy wielu instancji? Serverless i klastry mogą dawać wyniki pozornie losowe, ponieważ kolejne żądanie trafia do innego procesu. Zapisuj identyfikator instancji, czas oraz stan przed i po restarcie.

4. Poszukiwanie gadżetu

Przeglądaj odczyty właściwości o znaczeniu bezpieczeństwa. Interesują Cię obiekty opcji z brakującymi defaultami, pętle for...in, decyzje typu if (obj.flag) i wywołania wrażliwych API. Najpierw udowodnij przepływ neutralnym markerem. Dopiero potem — jeżeli jest to bezpieczne i uzgodnione — pokaż kontrolowany skutek na własnym koncie lub zasobie.

5. Sprzątanie i reprodukcja

Właściwość na prototypie może przetrwać do restartu procesu. Usuń ją jawnie, odtwórz instancję lub zrestartuj usługę testową. Raport powinien zawierać sposób przywrócenia stanu. W teście regresyjnym sprawdzaj zarówno odrzucenie złośliwego klucza, jak i brak odczytu wartości odziedziczonych.

Jak naprawić prototype pollution u źródła

Waliduj pełny schemat

Najsilniejszą kontrolą wejścia jest allowlista pól i typów. W JSON Schema ustawienie additionalProperties: false odrzuca nieoczekiwane klucze, jeżeli walidator jest poprawnie użyty. W bibliotekach takich jak Zod analogicznie definiuje się ścisły obiekt. Walidację trzeba wykonać przed merge, mapowaniem i logiką domenową.

Nie ograniczaj się do blacklisty __proto__. Ścieżka constructor.prototype i warianty zagnieżdżone mogą ominąć zbyt prosty filtr. Jeżeli aplikacja musi przyjmować dynamiczne klucze, waliduj je allowlistą znaków i nazw, a specjalne segmenty odrzucaj na każdym poziomie.

Odczytuj tylko własne właściwości

Object.hasOwn(obj, key) pozwala odróżnić dane obiektu od łańcucha prototypów. Object.keys() i iteracja for...of po tej liście są bezpieczniejszym wyborem niż for...in, które obejmuje właściwości enumerable odziedziczone z prototypu.

const isAdmin = Object.hasOwn(user, "isAdmin") && user.isAdmin === true;

Najlepiej także ustawiać jawne wartości domyślne. Jeżeli isAdmin zawsze istnieje i ma typ boolean, logika nie musi interpretować braku pola.

Używaj struktur bez prototypu

Dla słowników z dynamicznymi kluczami rozważ Map albo Object.create(null). Obiekt z prototypem null nie dziedziczy z Object.prototype. To nie zwalnia z walidacji wartości, ale usuwa ważną klasę kolizji nazw.

Ogranicz mechanizmy runtime

Node.js oferuje flagę --disable-proto=delete albo --disable-proto=throw, opisaną również przez MDN. Usuwa ona jeden punkt wejścia przez Object.prototype.__proto__, lecz nie blokuje ścieżki constructor.prototype. Jest warstwą hardeningu, nie pełną naprawą.

W środowiskach wysokiego zaufania można rozważyć zamrożenie wbudowanych obiektów lub mechanizmy typu SES/realm lockdown. Trzeba jednak przetestować kompatybilność bibliotek, ponieważ część kodu legalnie rozszerza prototypy.

Bezpieczny wzorzec projektowy

Ryzykowny wzorzecLepszy wzorzec
dowolny JSON scalany z konfiguracjąścisły DTO z allowlistą pól
target[userKey] = valueMap lub zweryfikowany klucz
if (user.isAdmin)własna właściwość + ścisły boolean
for...in po danychObject.keys() + for...of
częściowa konfiguracja bez defaultówpełny, niezmienny obiekt opcji
wiara w aktualną wersję bibliotekitest regresyjny source + gadget

Secure by Design oznacza tutaj, że granica zaufania kończy się przed utworzeniem obiektu domenowego. Kontroler HTTP nie powinien przekazywać surowego requestu do głębokiego merge. Warstwa mapująca wybiera znane pola, konwertuje typy i tworzy nowy obiekt o przewidywalnym kształcie.

Detekcja i monitoring

Logowanie całych payloadów może ujawnić dane osobowe, więc lepiej rejestrować naruszenie schematu, nazwę odrzuconego pola, endpoint i identyfikator korelacyjny. Alertuj na specjalne segmenty kluczy, ale pamiętaj, że brak tych ciągów w logach nie dowodzi bezpieczeństwa.

W CI dodaj testy dla __proto__, constructor, prototype, zagnieżdżonych ścieżek oraz nieznanych właściwości. Po próbie sprawdzaj, że pusty obiekt nie zawiera markera i że logika autoryzacyjna odczytuje wyłącznie własne pola. OWASP ASVS 5.0 może pomóc przypisać wymagania do walidacji, integralności i kontroli dostępu.

Frontend i backend mają inny zasięg skutków

Client-side prototype pollution zwykle żyje w kontekście strony i może prowadzić do DOM XSS, zmiany konfiguracji requestu albo obejścia logiki interfejsu. Nie należy jednak uznawać walidacji w przeglądarce za kontrolę serwerową — atakujący może wysłać request bez frontendu.

Server-side pollution w Node.js może dotknąć wiele żądań obsługiwanych przez ten sam proces. Skutek bywa trwały do restartu workera i trudny do odtworzenia w klastrze, bo ruch trafia do różnych instancji. W raporcie zaznacz, gdzie wykonano próbę, który proces został skażony i czy marker przeniósł się między sesjami. To odróżnia problem lokalnego obiektu od globalnej zmiany stanu usługi.

W aplikacji izomorficznej ta sama biblioteka może działać po obu stronach. Poprawka zależności powinna zatem objąć bundle przeglądarkowy, serwer SSR, narzędzia build i funkcje edge, a test regresyjny — każdy rzeczywisty runtime.

Checklista dla zespołu JavaScript

  • Czy każdy payload przechodzi ścisłą walidację przed scalaniem?
  • Czy odrzucasz nieznane pola i specjalne segmenty na każdym poziomie?
  • Czy słowniki z dynamicznymi kluczami używają Map lub null-prototype?
  • Czy decyzje bezpieczeństwa opierają się na Object.hasOwn() i ścisłych typach?
  • Czy obiekty konfiguracji mają komplet jawnych wartości domyślnych?
  • Czy zależności parserów i helperów merge są aktualizowane i testowane?
  • Czy proces można bezpiecznie odtworzyć po próbie skażenia?
  • Czy test regresyjny obejmuje osobno source, propagation i gadget?

Prototype pollution jest dobrym przykładem luki, której nie da się ocenić po samym payloadzie. Trzeba zrozumieć semantykę języka, przepływ danych i konkretne gadżety aplikacji. Jeżeli rozwijasz frontend lub backend Node.js, zamów test penetracyjny aplikacji — połączymy black-box z ukierunkowanym code review i wskażemy dokładne miejsce naprawy.

UDOSTĘPNIJ / KOPIUJ