JSON Toolkit

الأسئلة الشائعة حول الأداء والأمان: البطء والتقطع والبيانات غير الموثوقة

الخلاصة: لماذا لا يفتح ملف JSON بحجم عشرات MB، وكم يكلف الاستنساخ العميق، وكيف نمنع تلويث __proto__، ومن أين يدخل XSS — عشرة استنتاجات إسعاف أولي مرتبة حسب العَرَض.

متوسطقراءة نحو 13 دقيقةنُشر في 2026-10-30حُدِّث في 2026-10-30
صورة غلاف للأسئلة الشائعة حول الأداء والأمان

الخلاصة: شخّص حسب العَرَض أولًا — التحليل أم العرض أم الذاكرة — ثم امشِ في ترتيب الإصلاح: قلّل حجم البيانات، وأوقف الاستنساخ العميق، وانقل التحليل إلى Worker مع التمرير الافتراضي، وتعامل بالتدفق مع كل ما يزيد على 50 MB. وللمدخلات غير الموثوقة، حدّد الحجم وعمق التداخل قبل ترشيح __proto__ وconstructor وprototype.

عبارة "JSON بطيء" غير دقيقة في تسع حالات من عشر؛ ما هو بطيء فعلًا هو عادة العرض أو الاستنساخ العميق أو ترتيب عمليات خاطئ.

1. لماذا هو بطيء

تحليل ملف JSON كبير بطيء. كيف أحسّنه؟

حدّد عنق الزجاجة أولًا، وتحدث عن التحسين ثانيًا. تختلف المراحل بفروق عدة مراتب:

المرحلة الزمن التقريبي لبيانات 10 MB لماذا هي عنق الزجاجة
نقل الشبكة يعتمد على عرض النطاق JSON غير المضغوط ينضغط عادة إلى نحو 1/5
JSON.parse 20–80 ms تنفيذ أصلي، سريع أصلًا
المرور التكراري / الاستنساخ العميق 200–2000 ms معالجة JS خالصة عقدة بعقدة
عرض DOM (عرض الشجرة) ثوانٍ، ويتجمد بسهولة عدد العقد ينفجر

لذا يجب أن يكون ترتيب التحسين:

  1. قلّل حجم البيانات: اجعل API يجزّئ الصفحات، ويعيد فقط الحقول المطلوبة، وفعّل gzip/brotli؛
  2. تجنّب الاستنساخ العميق: راجع القسم التالي؛ هذه التكلفة الخفية الأكثر شيوعًا؛
  3. العرض الكسول: في عرض الشجرة اعرض المستويات الموسّعة فقط بدلًا من توليد عشرات آلاف عقد DOM دفعة واحدة؛
  4. التدفق: لا تحلل كل شيء دفعة واحدة عندما تتجاوز البيانات 10 MB.

إذا كان عنق الزجاجة فعلًا JSON.parse، فهذا يعني أن البيانات عشرات MB أو أكثر، والتصرف الصحيح محلل تدفق:

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 بحجم عشرات MB ينهار المتصفح لحظة فتحه. ماذا الآن؟

أكد أولًا هل "ينهار" أم "يتجمد". الاثنان يحتاجان إلى معالجة مختلفة تمامًا:

  • التجمد: يعرض التبويب "الصفحة لا تستجيب" وقد يتعافى بعد عشرات الثواني. السبب مهام طويلة تشبع الخيط الرئيسي؛
  • الانهيار: يتحول التبويب إلى "Aw, Snap"، وعادةً يكون استنفاد الذاكرة (OOM).

في الحالتين، لا تعالجه على الخيط الرئيسي. انقل التحليل إلى Web Worker:

// worker.js
self.onmessage = (event) => {
  const parsed = JSON.parse(event.data);
  self.postMessage(parsed);
};

Worker يحل مشكلة "الواجهة لا تتجمد"، لا سقف الذاكرة. تبويب المتصفح عادة لديه 1–4 GB متاحة؛ وملف JSON بحجم 200 MB بعد تحليله إلى كائنات يمكن أن يشغل 1 GB أو أكثر (كل زوج مفتاح-قيمة يحمل كلفة كائن)، ومع العرض فوق ذلك يصل إلى الحد بسهولة.

الحل الحقيقي هو ألّا تحمّله كله أبدًا:

حجم البيانات المقاربة
< 5 MB حلّل بـJSON.parse مباشرة، وراقب العرض
5–50 MB تحليل في Worker + تمرير افتراضي
> 50 MB تحليل بالتدفق، واحتفظ فقط بالحقول التي تحتاجها
> 500 MB غيّر التنسيق (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);

إذا كان الأمر مجرد دمج تهيئة سطحي، فمعامل النشر هو الخيار الأرخص: { ...config, timeout: 3000 }.

لماذا توسيع عرض الشجرة متقطع جدًا؟

لأن كل عقدة عنصر DOM، وبمجرد وصول عدد العقد إلى عشرات الآلاف تنفجر كلفة التخطيط في المتصفح.

ملف JSON بحجم 10 MB قد يحتوي 50k–100k عقدة طرفية. حتى لو عرضت المنطقة المرئية فقط، فإن بناء DOM مرة واحدة يستغرق من مئات المللي ثانية إلى ثوانٍ، وكل تمرير لاحق يطلق إعادة تدفق.

المقاربة الصحيحة:

  1. التمرير الافتراضي: اعرض فقط الصفوف العشرات داخل منفذ العرض وأعد استخدام عقد DOM أثناء التمرير؛
  2. الطي افتراضيًا: وسّع المستويين الأولين فقط واعرض المستويات الأعمق عند الطلب؛
  3. حدّ من مقياس العرض: فوق 10k عقدة، اعرض "البيانات كبيرة جدًا، استخدم البحث لتحديدها" بدلًا من فرض العرض.

ولهذا أيضًا يتراجع محرر هذا الموقع بلطف مع الملفات الكبيرة — لا لأنه لا يستطيع عرضها تقنيًا، بل لأن عرضها سيكون غير قابل للاستخدام.

2. لماذا يتعرض للهجوم

كيف يعمل حقن __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 وما شابهها من عمليات بعد التحليل.

ثلاث دفاعات:

  1. رشّح أسماء المفاتيح: تخطَّ __proto__ وconstructor وprototype عند الدمج؛
  2. استخدم Object.create(null) كحاوية حتى لا يوجد سلسلة prototype لتلويثها؛
  3. استخدم 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 غير موثوق؟

افعل ذلك بهذا الترتيب — كلفة منخفضة وعائد مرتفع:

  1. حدّد الحجم: ارفض فورًا أي شيء يتجاوز الحد، دون قراءته في الذاكرة أولًا؛
  2. حدّد العمق: JSON متداخل مئات المستويات يمكن أن يطفح مكدس محلل تكراري؛
  3. حدّد عدد المفاتيح: يمنع هجمات تصادم التجزئة وتضخيم الذاكرة؛
  4. رشّح المفاتيح الخطرة: __proto__ وconstructor وprototype؛
  5. تحقق من البنية: مرّرها عبر Schema لتأكيد الأنواع والحقول المطلوبة؛
  6. لا تثق في Content-Type: النوع المعلن والمحتوى الفعلي قد يتناقضان.

فحص العمق رخيص؛ يمكنك استخدام ترشيح تعبير نمطي خشن قبل التحليل، أو المرور بعد التحليل:

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>') هروب HTML عند الإخراج
innerHTML في الواجهة el.innerHTML = data.html استخدم textContent أو نظّف
تضمين في سكربت مضمّن <script>var d = ${JSON.stringify(data)}</script> هرّب < و> و&

الثالث هو الأخفى. JSON.stringify لا يهرّب <، فبيانات تحتوي </script><script>alert(1)</script> يمكن أن تغلق وسم السكربت مبكرًا:

const data = { name: '</script><script>alert(1)</script>' };
// Output: <script>var d = {"name":"</script><script>alert(1)</script>"}</script>

الإصلاح هو تحويل الأحرف الخطرة إلى تسلسلات هروب Unicode:

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 في بعض التطبيقات عندما يكون العمق كبيرًا جدًا، ويفجّر التسلسل المكدس بالطريقة نفسها. يكفي [[[[...]]]] بحجم 1 KB لإسقاط محلل غير محمي.

الحد المعقول يعتمد على العمل:

نوع البيانات الحد المقترح
كائنات الأعمال العادية 10–20 مستوى
البنى الشجرية (الهياكل التنظيمية، والأدلة) 50–100 مستوى
بوابة API عامة 20 مستوى

ما هو اختطاف JSON؟

ثغرة تاريخية: كانت المتصفحات المبكرة تسمح لـ<script src> عبر الأصول بتحميل JSON وتنفيذه، وكان المهاجمون يسرقون البيانات بإعادة تعريف مُنشئ Array أو Object.prototype.__defineSetter__.

الحمايات الحديثة مكتملة إلى حد كبير:

  • لم تعد المتصفحات تسمح باختطاف مصفوفات حرفية بـ__defineSetter__؛
  • Content-Type: application/json الصحيح يمنع تنفيذ السكربت؛
  • X-Content-Type-Options: nosniff يمنع استنشاق MIME.

ما زالت الأنظمة القديمة تحمل خطرًا متبقيًا. نصيحة عملية: استخدم كائنًا في المستوى الأعلى، لا مصفوفة عارية. غلاف مثل {"data": [...]} يمنع أن يكون الرد عبارة JavaScript صالحة ({ يُعامل ككتلة كود)، فيعطّل الاختطاف على مستوى الصياغة.

ما مخاطر تسجيل JSON؟

ثلاثة:

  1. معلومات حساسة تُكتب على القرص: أرقام الهواتف وأرقام الهوية والرموز وكلمات المرور النصية تصل إلى نظام السجلات، الذي عادةً لديه صلاحيات أوسع ومدة احتفاظ أطول؛
  2. حقن السجلات: قيمة JSON تحتوي أسطرًا جديدة وبادئات سجلات مزورة تقسم سجلًا واحدًا إلى عدة وتلوّث مسار التدقيق؛
  3. انفجار الحجم: كتابة رد كامل بحجم 10 MB في السجل تملأ نظام السجلات سريعًا.

المقاربة:

// 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) تهرّب الأسطر الجديدة بنفسها، لكن التنقيح والاقتطاع ما زالا مسؤوليتك.

3. مرجع سريع

العَرَض تحقق أولًا من الإصلاح
تحليل بطيء حجم البيانات، وهل يتضمن استنساخًا عميقًا التقسيم/التدفق، وإزالة JSON.parse(JSON.stringify())
الصفحة تتجمد مهام طويلة على الخيط الرئيسي، وعدد العقد المعروضة Worker + تمرير افتراضي
التبويب ينهار استهلاك الذاكرة لا تحمّل كل شيء؛ انتقل إلى التدفق
الاستنساخ العميق يفقد البيانات Date وNaN وMap استخدم structuredClone
كائنات لديها خصائص إضافية فجأة تلويث prototype رشّح __proto__، واستخدم Map
سكربت ينبثق سلاسل نصية مسلسلة داخل HTML هرّب <، واستخدم textContent
طفح المكدس عمق التداخل قيّده إلى 10–20 مستوى
السجلات تمتلئ تسجيل الرد كاملًا اقتطاع + تنقيح