Parsuj a validuj
Použij JSON.parse nebo parser svého jazyka, nikdy eval. Pak ověř typy, povinné položky a limity schématem. Schéma není autorizace: uživatel nesmí sám rozhodovat, zda má roli admin nebo kterou cizí objednávku smí změnit.
const data = JSON.parse(text);
if (data === null || Array.isArray(data) || typeof data !== "object") {
throw new Error("Očekávám objekt");
}
const user = { name: data.name }; // vybírej schválené položky Klíč __proto__ a slučování
JSON.parse vytvoří z __proto__ obyčejnou vlastní položku. Riziko může přijít později: Object.assign do běžného objektu používá zápis přes setter, který pro tento klíč mění prototyp cíle. Nedělej neomezené hluboké slučování nedůvěryhodných objektů. Vybírej povolená pole a aktualizuj knihovny.
const input = JSON.parse('{"__proto__":{"admin":true}}');
console.log(Object.hasOwn(input, "__proto__")); // true
const target = Object.assign({}, input);
console.log(target.admin); // true, zděděné z prototypu cíle
console.log({}.admin); // undefined: tato ukázka nemění Object.prototype Prototype pollution při rekurzivním slučování
V JavaScriptu dědí běžné objekty z Object.prototype. Pokud kód zapíše vlastnost přímo do tohoto prototypu, objeví se ve všech objektech aplikace. Typický zdroj je rekurzivní slučování (deep merge) nastavení nebo těla požadavku, které prochází klíče vstupu a zapisuje je do cíle, včetně klíče __proto__.
function merge(cil, zdroj) {
for (const klic in zdroj) {
if (typeof zdroj[klic] === "object" && zdroj[klic] !== null) {
if (typeof cil[klic] !== "object" || cil[klic] === null) cil[klic] = {};
merge(cil[klic], zdroj[klic]);
} else {
cil[klic] = zdroj[klic];
}
}
return cil;
}
const nastaveni = { jazyk: "cs" };
const telo = JSON.parse('{"jazyk":"en","__proto__":{"admin":true}}');
merge(nastaveni, telo);
const uzivatel = {};
console.log(uzivatel.admin); // true — každý nový objekt
console.log(Object.prototype.admin); // true Výraz cil["__proto__"] nevrací vlastní položku, ale prototyp cíle, tedy Object.prototype. Rekurze pak zapíše admin: true do něj. Kód typu if (uzivatel.admin) nebo výchozí hodnoty čtené z objektu možností pak vidí hodnotu útočníka. Podle toho, kde aplikace vlastnost čte, vede znečištění k obejití kontroly, pádu serveru nebo spuštění kódu. Stejnou cestou může vést i constructor.prototype, pokud slučovací funkce sestupuje do funkcí.
const ZAKAZANE = new Set(["__proto__", "constructor", "prototype"]);
function merge(cil, zdroj) {
for (const klic of Object.keys(zdroj)) { // jen vlastní klíče
if (ZAKAZANE.has(klic)) throw new Error(`Nepovolený klíč ${klic}`);
const hodnota = zdroj[klic];
if (typeof hodnota === "object" && hodnota !== null && !Array.isArray(hodnota)) {
if (!Object.hasOwn(cil, klic) || typeof cil[klic] !== "object" || cil[klic] === null)
cil[klic] = Object.create(null); // objekt bez prototypu
merge(cil[klic], hodnota);
} else {
cil[klic] = hodnota;
}
}
return cil;
}
try {
merge({ jazyk: "cs" }, JSON.parse('{"jazyk":"en","__proto__":{"admin":true}}'));
} catch (e) { console.log(e.message); } // Nepovolený klíč __proto__
try {
merge({}, JSON.parse('{"constructor":{"prototype":{"admin":true}}}'));
} catch (e) { console.log(e.message); } // Nepovolený klíč constructor
console.log({}.admin); // undefined
const slovnik = Object.create(null);
slovnik["__proto__"] = { admin: true };
console.log(Object.keys(slovnik), {}.admin); // [ '__proto__' ] undefined
const mapa = new Map(Object.entries(JSON.parse('{"__proto__":1}')));
console.log(mapa.get("__proto__")); // 1 - Odmítni nebezpečné klíče
__proto__,constructoraprototypea procházej jen vlastní klíče (Object.keys, nefor…in). - Slovníky s klíči od uživatele ukládej do
Mapnebo do objektu zObject.create(null). Ten nemá prototyp, takže__proto__je u něj obyčejný klíč. - Schéma s
additionalProperties: false(viz klíčová slova JSON Schema) odmítne nečekané klíče ještě před slučováním. Lepší než slučovat celý vstup je i zde vybrat povolená pole, viz příklad níže. - Knihovny pro merge, klonování a nastavení podle cesty (
set(obj, "a.b.c", …)) měly v minulosti tuto chybu opakovaně. Aktualizuj je a sleduj bezpečnostní upozornění. - Node.js s volbou
--disable-proto=deleteodstraní přístupový__proto__zObject.prototype. Ukázka výše pak vypíšeundefined. Je to doplňková ochrana a nepomůže proti cestě přesconstructor.prototype.
Samotný JSON.parse prototyp nemění; vytvoří obyčejnou vlastní položku __proto__. Zranitelnost vzniká až v kódu, který s výsledkem dál pracuje. Jiné jazyky (Python, PHP, Java) prototypovou dědičnost nemají, obdobné riziko u nich představuje mass assignment, tedy přenesení nečekaných polí do modelu, viz $fillable v Laravelu.
Zdroje, duplicity a HTML
Nastav limit bajtů, hloubky, počtu položek a délky řetězců už na vstupu. Duplicitní klíče mohou různé parsery vyhodnotit jinak; pro citlivé protokoly je odmítej před ztrátou informace. Řetězec z JSON není bezpečné HTML, SQL ani shell argument. Použij bezpečné API pro konkrétní výstup.
Praktické ověření
Testuj nečekaná pole, pole místo objektu, null, velké hodnoty a odmítnutí nepovolených operací. Formátování JSON ani rozkódování tokenu neověřuje podpis. Při podepisování záleží i na dohodnuté serializaci a algoritmu.
Celý příklad výběru povolených polí
function readUser(text) {
if (Buffer.byteLength(text, "utf8") > 4096) throw new Error("Limit vstupu");
const data = JSON.parse(text);
if (data === null || typeof data !== "object" || Array.isArray(data))
throw new Error("Očekáván objekt");
const keys = Object.keys(data);
if (keys.some(key => key !== "name")) throw new Error("Nepovolené pole");
if (!Object.hasOwn(data, "name") || typeof data.name !== "string" || data.name.length > 100)
throw new Error("Neplatné jméno");
return {name: data.name};
}
console.log(readUser('{"name":"Eva"}'));
try { readUser('{"name":"Eva","__proto__":{"admin":true}}'); }
catch (error) { console.log(error.message); } První vstup vrátí objekt name=Eva. Druhý se odmítne jako nepovolené pole. Serverová role není vůbec součástí modelu, takže ji klient nemůže přenést do aktualizace. Buffer limit zde kontroluje již dostupný řetězec; na HTTP hranici musí existovat i limit při čtení bajtů, aby se obří tělo nejdřív celé nealokovalo.
JSON Schema může vyjádřit pravidla typu, délky a dalších vlastností. Výběr a konstrukce nového doménového objektu pak chrání navazující kód před neomezeným merge. Duplicitní klíče už po JSON.parse nemusí být rozpoznatelné; jejich zákaz musí zajišťovat parser/textová vrstva. Text z JSON předávej do DOM přes textContent, do SQL přes parametry a do shellu přes bezpečné argumentové API, nikoli interpolací.
Co zkontrolovat u deserializace a podpisů
Stanov maximum bajtů, hloubky, počtu položek a délky hodnot. Hluboké objekty mohou zatížit následný rekurzivní kód i tehdy, když parser text přijal. Neaktivuj deserializační režim, který podle nedůvěryhodného názvu typu vytváří libovolné aplikační třídy. To je vlastnost knihovny a konfigurace, nikoli povolená schopnost samotného JSON.
U podepisování se oba systémy musejí shodnout na bajtech nebo přesném kanonizačním profilu. Přeskupení klíčů může zachovat JSON hodnotu a změnit podpis textu. Pro webové tokeny použij JWS/JWT knihovnu; neověřuj důvěru samotným dekódováním Base64url. Limity datového a schémového procesoru testuj odděleně od autorizace.