پرسشهای متداول عملکرد و امنیت: کند، لرزان و داده نامعتمد
جمعبندی: چرا فایل JSON چند ده مگابایتی باز نمیشود، شبیهسازی عمیق چقدر گران است، چگونه از آلودگی __proto__ جلوگیری کنیم و XSS از کجا وارد میشود — ده نتیجه کمک اولیه که بر اساس نشانه مرتب شدهاند.

جمعبندی: ابتدا بر اساس نشانه تشخیص دهید — تجزیه، رندر یا حافظه — سپس ترتیب اصلاح را طی کنید: حجم داده را کم کنید، شبیهسازی عمیق را متوقف کنید، تجزیه را به Worker منتقل کنید همراه با اسکرول مجازی، و هر چیزی بالاتر از ۵۰ مگابایت را جریانی کنید. برای ورودی نامعتمد، پیش از فیلتر کردن __proto__، constructor و prototype اندازه و عمق تودرتویی را محدود کنید.
«JSON کند است» نُه بار از ده بار نادرست است؛ آنچه واقعاً کند است معمولاً رندر، شبیهسازی عمیق یا ترتیب اشتباه عملیات است.
۱. چرا کند است
تجزیه فایل بزرگ JSON کند است. چگونه بهینهاش کنم؟
اول گلوگاه را پیدا کنید، بعد از بهینهسازی حرف بزنید. مراحل چند مرتبه بزرگی تفاوت دارند:
| مرحله | زمان تقریبی برای ۱۰ مگابایت داده | چرا گلوگاه است |
|---|---|---|
| انتقال شبکه | به پهنای باند بستگی دارد | JSON فشردهنشده معمولاً تا حدود ۱/۵ فشرده میشود |
JSON.parse |
۲۰–۸۰ میلیثانیه | پیادهسازی بومی، از قبل سریع |
| پیمایش بازگشتی / شبیهسازی عمیق | ۲۰۰–۲۰۰۰ میلیثانیه | پردازش گرهبهگره با JS خالص |
| رندر DOM (نمای درختی) | ثانیهها، بهراحتی قفل میکند | تعداد گره منفجر میشود |
پس ترتیب بهینهسازی باید این باشد:
- حجم داده را کم کنید: API را صفحهبندی کنید، فقط فیلدهای لازم را برگردانید و gzip/brotli را فعال کنید؛
- از شبیهسازی عمیق پرهیز کنید: بخش بعد را ببینید؛ این رایجترین هزینه پنهان است؛
- رندر تنبل: در نمای درختی فقط سطوح بازشده را رندر کنید نه اینکه دهها هزار گره DOM را یکجا بسازید؛
- جریان: وقتی داده از ۱۰ مگابایت گذشت همهچیز را یکجا تجزیه نکنید.
اگر گلوگاه واقعاً JSON.parse است، یعنی داده از قبل دهها مگابایت یا بیشتر است و حرکت درست پارسر جریانی است:
import { parser } from 'stream-json';
import { streamArray } from 'stream-json/streamers/StreamArray';
// Handle array elements one by one; memory usage is independent of the total size
const pipeline = fs.createReadStream('big.json')
.pipe(parser())
.pipe(streamArray());
pipeline.on('data', ({ value }) => {
processItem(value);
});
JSON چند ده مگابایتی لحظه باز شدن مرورگر را از کار میاندازد. حالا چه؟
اول تأیید کنید «از کار میافتد» یا «قفل میکند». این دو برخورد کاملاً متفاوت میخواهند:
- قفل کردن: تب «صفحه پاسخ نمیدهد» نشان میدهد و ممکن است پس از دهها ثانیه بهبود یابد. علت، وظایف طولانی اشباعکننده رشته اصلی است؛
- از کار افتادن: تب به «Aw, Snap» تبدیل میشود، معمولاً اتمام حافظه (OOM).
در هر دو حالت، روی رشته اصلی پردازش نکنید. تجزیه را به Web Worker منتقل کنید:
// worker.js
self.onmessage = (event) => {
const parsed = JSON.parse(event.data);
self.postMessage(parsed);
};
Worker «قفل نشدن رابط کاربری» را حل میکند، نه سقف حافظه را. یک تب مرورگر معمولاً ۱ تا ۴ گیگابایت در دسترس دارد؛ JSON دویست مگابایتی که به شیء تجزیه شود میتواند ۱ گیگابایت یا بیشتر اشغال کند (هر جفت کلید-مقدار سربار شیء دارد)، و با رندر روی آن بهراحتی به محدودیت میخورد.
راهحل واقعی این است که هرگز همه را بارگذاری نکنید:
| اندازه داده | رویکرد |
|---|---|
| کمتر از ۵ مگابایت | مستقیم با JSON.parse تجزیه کنید، رندر را زیر نظر بگیرید |
| ۵ تا ۵۰ مگابایت | تجزیه در Worker + اسکرول مجازی |
| بیشتر از ۵۰ مگابایت | تجزیه جریانی، فقط فیلدهای لازم را نگه دارید |
| بیشتر از ۵۰۰ مگابایت | قالب را عوض کنید (Parquet / SQLite / JSON Lines تکهتکه) |
آیا JSON.parse(JSON.stringify(obj)) یک شبیهسازی عمیق است؟
شبیهسازی عمیق است، اما در چند جا «بیصدا داده را تغییر میدهد»، پس ابزار شبیهسازی عمومی مناسبی نیست.
const source = {
date: new Date(),
fn: () => {},
undef: undefined,
nan: NaN,
map: new Map(),
nested: { a: 1 },
};
JSON.parse(JSON.stringify(source));
// {
// date: "2026-10-30T03:00:00.000Z", // Date becomes a string
// nan: null, // NaN becomes null
// nested: { a: 1 } // only this level is correct
// }
// fn and undef disappear, map becomes {}
فهرست مشکلات:
| نوع | نتیجه |
|---|---|
Date |
رشته ISO میشود |
NaN / Infinity |
null میشود |
undefined / تابع / Symbol |
در اشیا ناپدید میشود، در آرایهها null میشود |
Map / Set |
{} میشود |
BigInt |
TypeError میاندازد |
| ارجاع چرخهای | TypeError میاندازد |
مرورگرهای مدرن structuredClone دارند که از Date، Map، Set و ارجاع چرخهای پشتیبانی میکند اما از تابع نه:
const copy = structuredClone(source);
اگر فقط ادغام سطحی پیکربندی است، عملگر spread ارزانترین گزینه است: { ...config, timeout: 3000 }.
چرا باز کردن نمای درختی اینقدر لرزان است؟
چون هر گره یک عنصر DOM است و وقتی تعداد گرهها به دهها هزار برسد هزینه چیدمان مرورگر منفجر میشود.
JSON ده مگابایتی میتواند ۵۰ تا ۱۰۰ هزار گره برگ داشته باشد. حتی اگر فقط ناحیه مرئی را رندر کنید، ساخت یکباره DOM صدها میلیثانیه تا ثانیهها طول میکشد، و هر اسکرول بعدی یک بازچینش را تحریک میکند.
رویکرد درست:
- اسکرول مجازی: فقط چند ده ردیف داخل viewport را رندر کنید و هنگام اسکرول گرههای DOM را بازاستفاده کنید؛
- جمعشدگی پیشفرض: فقط دو سطح اول را باز کنید و سطوح عمیقتر را در صورت تقاضا رندر کنید؛
- محدود کردن مقیاس رندر: بالاتر از ۱۰ هزار گره، «داده بیش از حد بزرگ است، برای مکانیابی از جستوجو استفاده کنید» نشان دهید نه اینکه رندر را تحمیل کنید.
این همچنین دلیل تخریب مؤدبانه ویرایشگر این سایت برای فایلهای بزرگ است — نه اینکه از نظر فنی نمیتواند رندرشان کند، بلکه رندرشان غیرقابل استفاده میشد.
۲. چرا مورد حمله قرار میگیرد
تزریق __proto__ چگونه کار میکند؟
آلودگی prototype. مهاجم JSONی با کلید __proto__ میسازد؛ اگر کد شما آن را بدون فیلتر در شیئی ادغام کند،
Object.prototype را بازنویسی میکند و روی همه اشیای زمان اجرا اثر میگذارد.
const payload = JSON.parse('{"__proto__": {"isAdmin": true}}');
// Dangerous code: recursive merge without checking key names
function merge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object') {
target[key] = target[key] || {};
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
merge({}, payload);
({}).isAdmin; // true — every object has been polluted
نکته کلیدی: خود JSON.parse prototype را آلوده نمیکند؛ {"__proto__": ...} به یک ویژگی معمولی خودی تجزیه میشود.
آنچه خطرناک است ادغام، انتساب، Object.assign، lodash.merge و عملیات مشابه پس از تجزیه است.
سه دفاع:
- فیلتر نام کلید: هنگام ادغام
__proto__،constructorوprototypeرا رد کنید؛ - استفاده از
Object.create(null)بهعنوان ظرف تا زنجیره prototypeای برای آلوده کردن نباشد؛ - استفاده از
Mapبرای ذخیره جفتهای کلید-مقدار نامعتمد.
function safeMerge(target, source) {
for (const key of Object.keys(source)) {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
continue;
}
const value = source[key];
if (value && typeof value === 'object' && !Array.isArray(value)) {
target[key] = safeMerge(target[key] ?? {}, value);
} else {
target[key] = value;
}
}
return target;
}
همچنین توجه کنید for...in ویژگیهای ارثی را طی میکند؛ Object.keys() امنتر است.
هنگام تجزیه JSON نامعتمد به چه چیزی باید توجه کنم؟
به این ترتیب انجام دهید — هزینه کم، سود بالا:
- محدود کردن اندازه: هر چیزی بالاتر از آستانه را فوراً رد کنید، بدون اینکه اول در حافظه بخوانید؛
- محدود کردن عمق: JSON با صدها سطح تودرتویی میتواند پشته پارسر بازگشتی را سرریز کند؛
- محدود کردن تعداد کلید: از حملات برخورد هش و بزرگنمایی حافظه جلوگیری میکند؛
- فیلتر کلیدهای خطرناک:
__proto__،constructor،prototype؛ - اعتبارسنجی ساختار: آن را از Schema عبور دهید تا نوعها و فیلدهای الزامی تأیید شوند؛
- به
Content-Typeاعتماد نکنید: نوع اعلامشده و محتوای واقعی ممکن است نخوانند.
بررسی عمق ارزان است؛ میتوانید پیش از تجزیه از فیلتر regex درشت استفاده کنید یا پس از تجزیه پیمایش کنید:
function measureDepth(value, depth = 0) {
if (value === null || typeof value !== 'object') {
return depth;
}
let maxDepth = depth;
for (const key of Object.keys(value)) {
maxDepth = Math.max(maxDepth, measureDepth(value[key], depth + 1));
}
return maxDepth;
}
اما این کد خودش بازگشتی است و ممکن است روی داده عمیقاً مخرب همچنان پشته را سرریز کند. رویکرد امن اعمال محدودیت هنگام تجزیه است،
با پارسری که گزینه عمق دارد (برخی کتابخانهها پارامتر maxDepth ارائه میدهند).
XSS از طریق JSON چگونه رخ میدهد؟
خود JSON HTML نیست و اجرا نمیشود. XSS در نقطهای رخ میدهد که «مقدار JSON در HTML بههم چسبانده میشود».
سه مسیر رایج:
| مسیر | مثال | دفاع |
|---|---|---|
| الحاق قالب در سرور | res.send('<div>' + data.name + '</div>') |
escape HTML در خروجی |
innerHTML فرانتاند |
el.innerHTML = data.html |
از textContent یا sanitize استفاده کنید |
| جاسازی اسکریپت درونخطی | <script>var d = ${JSON.stringify(data)}</script> |
escape کردن <، >، & |
سومی ظریفترین است. JSON.stringify نویسه < را escape نمیکند، پس دادهای شامل </script><script>alert(1)</script>
میتواند تگ اسکریپت را زودتر ببندد:
const data = { name: '</script><script>alert(1)</script>' };
// Output: <script>var d = {"name":"</script><script>alert(1)</script>"}</script>
راهحل تبدیل نویسههای خطرناک به escape یونیکد است:
function safeJsonForHtml(data) {
return JSON.stringify(data)
.replace(/</g, '\\u003c')
.replace(/>/g, '\\u003e')
.replace(/&/g, '\\u0026')
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029');
}
آیا باید عمق تودرتویی را محدود کنم؟
بله. این یکی از دفاعهایی است که بهترین نسبت هزینه به سود را دارد.
پیامدهای محدود نکردن: توابع بازگشتی پشته را سرریز میکنند، JSON.parse در برخی پیادهسازیها وقتی عمق زیاد است از کار میافتد،
و سریالایز هم به همین شکل پشته را منفجر میکند. [[[[...]]]] یککیلوبایتی برای از کار انداختن پارسر بیدفاع کافی است.
آستانه معقول به کسبوکار بستگی دارد:
| نوع داده | محدودیت پیشنهادی |
|---|---|
| اشیای معمول کسبوکار | ۱۰ تا ۲۰ سطح |
| ساختارهای درختی (چارت سازمانی، پوشهها) | ۵۰ تا ۱۰۰ سطح |
| دروازه عمومی API | ۲۰ سطح |
JSON hijacking چیست؟
آسیبپذیری تاریخی: مرورگرهای اولیه اجازه میدادند <script src> بینمبدأ JSON را بارگذاری و اجرا کند و مهاجمان با بازتعریف
سازنده Array یا Object.prototype.__defineSetter__ داده را میدزدیدند.
دفاعهای مدرن نسبتاً کاملاند:
- مرورگرها دیگر اجازه hijacking آرایهها با
__defineSetter__را نمیدهند؛ Content-Type: application/jsonدرست از اجرای اسکریپت جلوگیری میکند؛X-Content-Type-Options: nosniffاز sniffing نوع MIME جلوگیری میکند.
سیستمهای قدیمی همچنان ریسک باقیمانده دارند. توصیه عملی: در سطح بالا از شیء استفاده کنید، نه آرایه خالی.
پاکتی مانند {"data": [...]} مانع میشود پاسخ یک دستور جاوااسکریپت معتبر باشد ({ بهعنوان بلوک کد تلقی میشود)
و hijacking را در سطح نحو مسدود میکند.
ثبت لاگ JSON چه ریسکهایی دارد؟
سه مورد:
- اطلاعات حساس روی دیسک نوشته میشود: شماره تلفن، شماره شناسایی، توکن و گذرواژه متنی در سیستم لاگ مینشیند که معمولاً مجوزهای سهلگیرتر و نگهداری طولانیتر دارد؛
- تزریق لاگ: ارزش JSON شامل خط جدید و پیشوند لاگ جعلی یک رکورد را به چند تکه میشکند و رد حسابرسی را آلوده میکند؛
- انفجار حجم: نوشتن کل پاسخ ده مگابایتی در لاگ، سیستم لاگ را سریع پر میکند.
رویکرد:
// Redact and truncate before logging
function safeLog(data) {
const cloned = structuredClone(data);
for (const key of ['password', 'token', 'idCard', 'phone']) {
if (key in cloned) cloned[key] = '***';
}
const text = JSON.stringify(cloned);
return text.length > 2000 ? text.slice(0, 2000) + '...(truncated)' : text;
}
سیستمهای لاگ ساختاریافته (مانند pino) خودشان خطوط جدید را escape میکنند، اما حذف اطلاعات حساس و کوتاهسازی همچنان کار شماست.
۳. مرجع سریع
| نشانه | اول چه بررسی شود | اصلاح |
|---|---|---|
| تجزیه کند | حجم داده، دخیل بودن شبیهسازی عمیق | تکهبندی/جریان، حذف JSON.parse(JSON.stringify()) |
| صفحه قفل میکند | وظایف طولانی روی رشته اصلی، تعداد گره رندرشده | Worker + اسکرول مجازی |
| تب از کار میافتد | مصرف حافظه | همه را بارگذاری نکنید؛ به جریان بروید |
| شبیهسازی عمیق داده را از دست میدهد | Date، NaN، Map |
از structuredClone استفاده کنید |
| اشیا ناگهان ویژگی اضافه دارند | آلودگی prototype | __proto__ را فیلتر کنید، از Map استفاده کنید |
| اسکریپتی باز میشود | رشتههای الحاقشده به HTML | < را escape کنید، از textContent استفاده کنید |
| سرریز پشته | عمق تودرتویی | به ۱۰ تا ۲۰ سطح محدود کنید |
| لاگها پر میشوند | کل پاسخ لاگ میشود | کوتاهسازی + حذف اطلاعات حساس |