Senarai Semak Risiko Keselamatan untuk Pemprosesan JSON
Inti pati: JSON.parse sendiri selamat; bahayanya ialah apa yang berlaku selepasnya: cantum mendalam, penetapan mengikut laluan, pembenaman ke dalam HTML, kedalaman dan saiz tanpa had.

Kesimpulan utama: tutup dua titik masuk dahulu — setiap cantum mendalam dan penetapan laluan mesti menolak proto, prototype, dan constructor, dan saiz badan permintaan serta kedalaman bersarang memerlukan had keras yang terpakai sebelum penghuraian. Memerhati JSON.parse sendiri membiarkan pintu berbahaya terbuka luas.
Penyerang tidak perlu mencipta kerentanan baharu; mereka hanya menambah satu lagi kunci pada JSON.
1. Pencemaran Prototaip: Yang Paling Biasa dan Paling Terlepas Pandang
1.1 JSON.parse sendiri selamat
const data = JSON.parse('{"__proto__": {"polluted": true}}');
console.log(data.polluted); // undefined
console.log({}.polluted); // undefined
JSON.parse mencipta __proto__ sebagai sifat sendiri biasa dan tidak menulis semula rantaian prototaip. Jadi dakwaan bahawa "JSON.parse menyebabkan pencemaran prototaip" tidak tepat.
1.2 Cantum selepas itu yang berbahaya
/**
* Unsafe deep merge: writes the source object's keys straight into the target object.
* @param target Target object
* @param source Source object (may come from user JSON)
* @returns Merged object
*/
function unsafeMerge(target, source) {
for (const key of Object.keys(source)) {
if (typeof source[key] === 'object' && source[key] !== null) {
target[key] = target[key] ?? {};
unsafeMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
unsafeMerge({}, JSON.parse('{"__proto__": {"isAdmin": true}}'));
console.log({}.isAdmin); // true — the entire site is polluted
constructor.prototype ialah titik masuk lain yang sama jenis:
{ "constructor": { "prototype": { "isAdmin": true } } }
1.3 Cara membaikinya
/** Key names that must be rejected */
const FORBIDDEN_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
/**
* Safe deep merge: rejects prototype-related keys and handles own properties only.
* @param target Target object
* @param source Source object
* @returns New merged object
*/
function safeMerge(target, source) {
const result = { ...target };
for (const key of Object.keys(source)) {
if (FORBIDDEN_KEYS.has(key)) {
continue;
}
const sourceValue = source[key];
const isPlainObject =
typeof sourceValue === 'object' && sourceValue !== null && !Array.isArray(sourceValue);
if (isPlainObject) {
result[key] = safeMerge(target[key] ?? {}, sourceValue);
continue;
}
// Use defineProperty so a setter cannot trigger prototype mutation
Object.defineProperty(result, key, {
value: sourceValue,
enumerable: true,
writable: true,
configurable: true,
});
}
return result;
}
Pendekatan yang lebih menyeluruh ialah menggunakan Object.create(null) sebagai bekas (tiada rantaian prototaip untuk dicemari), atau cukup Map.
Apabila menggunakan pustaka sedia ada, pastikan ia sudah mengendalikan tiga kunci ini — banyak versi lama merge lodash dilaporkan kerana perkara ini.
1.4 Penetapan mengikut laluan sama berbahaya
// Anti-pattern: the path comes from user input
for (const segment of pathSegments) {
current = current[segment] ?? (current[segment] = {});
}
Apabila __proto__ muncul dalam pathSegments, ini setara dengan pencemaran di atas. Pelaksanaan JSON Pointer / JSON Patch mesti mengesahkan setiap segmen token.
2. Serangan Kehabisan Sumber
| Serangan | Taktik | Mitigasi |
|---|---|---|
| Bersarang dalam | Seratus ribu tahap [[[[...]]]] |
Hadkan kedalaman bersarang (biasanya 32–64) dan pra-imbas sebelum penghuraian |
| Tatasusunan terlalu besar | Tatasusunan dengan puluhan juta elemen | Hadkan saiz badan permintaan dan panjang tatasusunan |
| String sangat panjang | Satu string beratus-ratus MB | Hadkan panjang medan |
| Letupan kunci pendua | Sejumlah besar kunci pendua | Hadkan bilangan kunci objek |
Dua pertahanan paling berkesan pada pelayan ialah had saiz badan permintaan dan had kedalaman, dan kedua-duanya mesti berkuat kuasa sebelum sebarang penghuraian berlaku.
/**
* Cheaply checks nesting depth before parsing to prevent stack overflow during parsing.
* @param text JSON text
* @param maxDepth Maximum allowed depth
* @returns Whether the depth is within the allowed range
*/
function isDepthAcceptable(text: string, maxDepth = 64): boolean {
let currentDepth = 0;
let isInsideString = false;
let isEscaped = false;
for (const character of text) {
if (isInsideString) {
if (isEscaped) {
isEscaped = false;
} else if (character === '\\') {
isEscaped = true;
} else if (character === '"') {
isInsideString = false;
}
continue;
}
if (character === '"') {
isInsideString = true;
} else if (character === '{' || character === '[') {
currentDepth += 1;
if (currentDepth > maxDepth) {
return false;
}
} else if (character === '}' || character === ']') {
currentDepth -= 1;
}
}
return true;
}
3. Suntikan Apabila Membenamkan ke dalam HTML
Menulis JSON terus ke dalam halaman ialah amalan biasa, tetapi ia memperkenalkan tiga kelas masalah:
<script>
window.__DATA__ = {"title": "</script><script>alert(1)</script>"};
</script>
</script> di dalam string menutup blok skrip lebih awal, dan selebihnya dihurai sebagai HTML — XSS klasik.
Dua aksara lagi berkelakuan sama: \u2028 (pemisah baris) dan \u2029 (pemisah perenggan) sah dalam JSON, tetapi membenamkannya dalam sumber JS mencetuskan kesalahan sintaks.
Pembaikan:
/**
* Serializes JSON safely for an inline script.
* @param value Data object
* @returns String that can be safely placed inside a script tag
*/
function serializeForInlineScript(value: unknown): string {
return JSON.stringify(value)
.replace(/</g, '\\u003c')
.replace(/>/g, '\\u003e')
.replace(/&/g, '\\u0026')
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029');
}
Pendekatan yang lebih mantap ialah meletakkan data di dalam <script type="application/json"> (yang kandungannya tidak dilaksanakan), kemudian membacanya dengan JSON.parse(document.getElementById('data').textContent).
4. Perampasan JSON dan JSONP
Pelayar awal membenarkan <script src="https://api.example.com/user"> merentas asal melaksanakan JavaScript yang dipulangkan secara langsung,
dan JSON yang tahap atasnya ialah tatasusunan kebetulan juga ungkapan JS yang sah, jadi penyerang boleh membaca data dengan menimpa pembina Array.
Pelayar moden sudah mempunyai perlindungan, tetapi kami tetap mengesyorkan:
- Gunakan objek dan bukannya tatasusunan pada tahap atas API (
{"data": [...]}); - Hantar
X-Content-Type-Options: nosniffdan tetapkanContent-Type: application/jsonsecara ketat; - Berhenti menggunakan JSONP; gunakan CORS apabila akses merentas asal benar-benar diperlukan.
5. Kekeliruan Jenis Semasa Nyahseri ke dalam Objek
Sebaik penjenisan lalai diaktifkan dalam Jackson (enableDefaultTyping), penyerang boleh menentukan medan @class untuk membuat pelayan instanti kelas arbitrari,
dan menggabungkannya dengan rantaian gadget yang boleh dieksploitasi untuk mencapai pelaksanaan kod jauh. Kerentanan jenis ini telah muncul berkali-kali (Fastjson dan Jackson kedua-duanya pernah terjejas).
Prinsip: jenis sasaran penyahserian mesti dinyatakan secara statik oleh kod pelayan dan tidak boleh sekali-kali ditentukan oleh kandungan muatan.
6. Penafian Perkhidmatan dalam Ungkapan Nalar dan Skema
patternJSON Schema danmatch()JSONPath kedua-duanya menerima ungkapan nalar;- Corak seperti
(a+)+$mundur secara eksponen pada input panjang; - Jika regex datang daripada pengguna (peraturan pengesahan tersuai), anda mesti mengehadkan panjangnya, melarang rujukan belakang dan pengkuantiti bersarang, atau beralih ke enjin linear seperti RE2.
7. Pencatatan Log dan Penopengan
Log JSON ialah cara paling mudah untuk membocorkan maklumat sensitif:
/** Field names that must be masked in logs (matched in lowercase) */
const SENSITIVE_KEYS = new Set(['password', 'token', 'secret', 'idcard', 'phone', 'authorization']);
/**
* Recursively masks sensitive fields in an object for log output.
* @param value Original value
* @returns Masked copy
*/
function maskSensitive(value: unknown): unknown {
if (Array.isArray(value)) {
return value.map((item) => maskSensitive(item));
}
if (typeof value === 'object' && value !== null) {
return Object.fromEntries(
Object.entries(value).map(([entryKey, entryValue]) => {
if (SENSITIVE_KEYS.has(entryKey.toLowerCase())) {
return [entryKey, '***'];
}
return [entryKey, maskSensitive(entryValue)];
}),
);
}
return value;
}
Perhatikan bahawa penopengan mesti berlaku sebelum log ditulis, bukan dengan membersihkan platform log selepas itu.
8. Senarai Semak Sebelum Pelancaran
- [ ] Semua fungsi cantum mendalam menolak
__proto__/prototype/constructor; - [ ] Token laluan JSON Pointer / Patch disahkan terhadap senarai putih;
- [ ] Saiz badan permintaan dan kedalaman bersarang mempunyai had keras, dan had itu terpakai sebelum penghuraian;
- [ ] JSON yang dibenamkan ke dalam HTML mengescape
<,>,&,U+2028, danU+2029; - [ ] API menggunakan objek pada tahap atas dan respons menghantar
nosniff; - [ ] Jenis sasaran penyahserian dinyatakan secara statik dalam kod;
- [ ] Ungkapan nalar yang dikawal pengguna mempunyai had kerumitan;
- [ ] Medan log mempunyai peraturan penopengan dan peraturan itu diuji.
FAQ
Bagaimana saya menyemak sama ada cantum mendalam saya terdedah kepada pencemaran prototaip?
Suapkan JSON.parse('{"__proto__": {"isAdmin": true}}') ke dalam fungsi cantum, kemudian semak sama ada {}.isAdmin menjadi true; ulangi dengan muatan constructor.prototype. Dengan pustaka pihak ketiga, pastikan ia menolak __proto__, prototype, dan constructor — banyak versi lama merge lodash dilaporkan kerana perkara ini. Penetapan berasaskan laluan memerlukan semakan sendiri, kerana token __proto__ dalam laluan mencemari dengan cara yang sama.
Mengapa JSON.parse sendiri bukan punca pencemaran prototaip?
Ia mencipta __proto__ sebagai sifat sendiri biasa dan tidak pernah menulis semula rantaian prototaip, jadi data.polluted dan {}.polluted kedua-duanya kekal undefined. Pencemaran berlaku selepas itu: cantum mendalam yang tidak selamat menulis kunci objek sumber terus ke dalam sasaran, dan constructor.prototype ialah titik masuk yang sama jenis. Menganggap JSON.parse sebagai punca utama bermakna membetulkan tempat yang salah.
Patutkah JSON yang dibenamkan ke dalam HTML sentiasa diescape?
Ya. </script> di dalam string menutup blok skrip lebih awal dan selebihnya dihurai sebagai HTML — XSS klasik; <, >, &, U+2028, dan U+2029 juga perlu diescape. Pilihan lebih selamat ialah meletakkan data di dalam <script type="application/json">, yang kandungannya tidak dilaksanakan, dan membacanya dengan JSON.parse pada textContent.