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 קורס את הדפדפן ברגע הפתיחה. מה עכשיו?

קודם ודאו אם הוא "קורס" או "קופא". השניים דורשים טיפול שונה לחלוטין:

  • קופא: הלשונית מציגה "Page unresponsive" ועשויה להתאושש אחרי עשרות שניות. הסיבה היא משימות ארוכות שמעמיסות על ה-thread הראשי;
  • קורס: הלשונית הופכת ל-"Aw, Snap", בדרך כלל מיצוי זיכרון (OOM).

בכל מקרה, אל תעבדו אותו ב-thread הראשי. העבירו את הפירוס ל-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);

אם זה רק מיזוג קונפיגורציה רדוד, אופרטור ה-spread הוא האפשרות הזולה ביותר: { ...config, timeout: 3000 }.

למה הרחבת תצוגת העץ כל כך מקרטעת?

כי כל צומת הוא אלמנט DOM, וברגע שמספר הצמתים מגיע לעשרות אלפים עלות הפריסה של הדפדפן מתפוצצת.

JSON של 10 MB יכול להכיל 50k–100k צמתי עלה. גם אם מרנדרים רק את האזור הנראה, בניית ה-DOM פעם אחת לוקחת מאות אלפיות שנייה ועד שניות, וכל גלילה לאחר מכן מפעילה reflow.

הגישה הנכונה:

  1. גלילה וירטואלית: רנדרו רק את עשרות השורות שבתוך אזור התצוגה ותעדו שימוש חוזר בצמתי DOM בזמן גלילה;
  2. כיווץ כברירת מחדל: הרחיבו רק את שתי הרמות הראשונות ורנדרו רמות עמוקות לפי דרישה;
  3. הגבילו את קנה המידה של הרינדור: מעל 10k צמתים, הציגו "הנתונים גדולים מדי, השתמשו בחיפוש כדי לאתר אותם" במקום לכפות רינדור.

זו גם הסיבה שהעורך באתר הזה מתנהג בחן עם קבצים גדולים — לא כי הוא טכנית לא יכול לרנדר אותם, אלא כי רינדור שלהם לא יהיה שמיש.

2. למה תוקפים אותו

איך עובדת הזרקת __proto__?

זיהום פרוטוטיפ. תוקף בונה 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 עצמו לא מזהם את הפרוטוטיפ; {"__proto__": ...} מתפרס למאפיין עצמי רגיל. הדבר המסוכן הוא מיזוג, השמה, Object.assign, lodash.merge ופעולות דומות אחרי הפירוס.

שלוש הגנות:

  1. סננו שמות מפתחות: דלגו על __proto__, constructor ו-prototype בזמן מיזוג;
  2. השתמשו ב-Object.create(null) כמכולה כך שאין שרשרת פרוטוטיפ לזהם;
  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. הגבילו את מספר המפתחות: מונע התקפות התנגשות hash והגדלת זיכרון;
  4. סננו מפתחות מסוכנים: __proto__, constructor, prototype;
  5. אמתו את המבנה: העבירו אותו דרך Schema כדי לאשר טיפוסים ושדות חובה;
  6. אל תסמכו על 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>') קידוד HTML בפלט
innerHTML בחזית el.innerHTML = data.html השתמשו ב-textContent או בניקוי
הטמעת סקריפט inline <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 hijacking?

פגיעות היסטורית: דפדפנים מוקדמים אפשרו ל-<script src> חוצה-מקור לטעון ולהריץ JSON, ותוקפים גנבו נתונים על ידי הגדרה מחדש של ה-constructor של Array או של Object.prototype.__defineSetter__.

ההגנות המודרניות די שלמות:

  • דפדפנים כבר לא מאפשרים לחטוף מערכים ליטרליים עם __defineSetter__;
  • Content-Type: application/json תקין מונע ביצוע סקריפט;
  • X-Content-Type-Options: nosniff מונע MIME sniffing.

מערכות ישנות עדיין נושאות סיכון שיורי. עצה מעשית: השתמשו באובייקט ברמה העליונה, לא במערך חשוף. עטיפה כמו {"data": [...]} מונעת מהתשובה להיות הצהרת JavaScript תקינה ({ מטופל כבלוק קוד), וחוסמת חטיפה ברמת התחביר.

מהם הסיכונים של רישום JSON ללוג?

שלושה:

  1. מידע רגיש שנכתב לדיסק: מספרי טלפון, מספרי זהות, tokens וסיסמאות בטקסט גלוי מגיעים למערכת הלוגים, שלה בדרך כלל הרשאות רופפות יותר ושימור ארוך יותר;
  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())
העמוד קופא משימות ארוכות ב-thread הראשי, מספר צמתים מרונדרים Worker + גלילה וירטואלית
הלשונית קורסת שימוש בזיכרון אל תטענו הכל; עברו לזרימה
שכפול עמוק מאבד נתונים Date, NaN, Map השתמשו ב-structuredClone
לאובייקטים פתאום יש מאפיינים נוספים זיהום פרוטוטיפ סננו __proto__, השתמשו ב-Map
סקריפט קופץ מחרוזות שמשורשרות ל-HTML קדדו <, השתמשו ב-textContent
הצפת מחסנית עומק קינון הגבילו ל-10–20 רמות
הלוגים מתמלאים כל התשובה נרשמת ללוג קצצו + השחרת מידע