JSON Toolkit

JSON প্রসেসিংয়ের নিরাপত্তা ঝুঁকি চেকলিস্ট

সংক্ষেপে: JSON.parse নিজে নিরাপদ; বিপদ তার পরে যা ঘটে তাতে: গভীর মার্জ, পাথ দিয়ে অ্যাসাইনমেন্ট, HTML-এ ইনলাইন, অসীম গভীরতা ও আকার।

মধ্যবর্তী~12 মিনিট পড়াপ্রকাশ 2026-10-30আপডেট 2026-10-30
JSON প্রসেসিংয়ের নিরাপত্তা ঝুঁকি চেকলিস্টের কভার

মূল কথা: আগে দুই প্রবেশবিন্দু বন্ধ করুন — প্রতিটি গভীর মার্জ ও পাথ-অ্যাসাইনমেন্টে __proto__, prototype ও constructor প্রত্যাখ্যান করতে হবে, আর রিকোয়েস্ট বডির আকার ও নেস্টিং গভীরতার কঠোর সীমা পার্সিংয়ের আগেই কার্যকর হতে হবে। কেবল JSON.parse-এর দিকে তাকিয়ে থাকলে বিপজ্জনক দরজাগুলো পুরো খোলা থাকে।

আক্রমণকারীদের নতুন দুর্বলতা উদ্ভাবন করতে হয় না; তারা কেবল JSON-এ আরেকটি কি যোগ করে।

১. প্রোটোটাইপ দূষণ: সবচেয়ে প্রচলিত ও সবচেয়ে উপেক্ষিত

১.১ JSON.parse নিজে নিরাপদ

const data = JSON.parse('{"__proto__": {"polluted": true}}');
console.log(data.polluted);        // undefined
console.log({}.polluted);          // undefined

JSON.parse __proto__-কে সাধারণ নিজস্ব প্রপার্টি হিসেবে তৈরি করে, প্রোটোটাইপ চেইন আবার লেখে না। তাই "JSON.parse প্রোটোটাইপ দূষণ ঘটায়" দাবিটি ভুল।

১.২ যেটি বিপজ্জনক তা হলো পরের মার্জ

/**
 * 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 একই ধরনের আরেকটি প্রবেশবিন্দু:

{ "constructor": { "prototype": { "isAdmin": true } } }

১.৩ কীভাবে ঠিক করবেন

/** 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;
}

আরও পূর্ণাঙ্গ পথ হলো কন্টেইনার হিসেবে Object.create(null) ব্যবহার করা (দূষণের জন্য কোনো প্রোটোটাইপ চেইন নেই), বা সোজা Map। প্রস্তুত লাইব্রেরি ব্যবহার করলে নিশ্চিত করুন এটি এই তিন কি ইতিমধ্যে সামলায় — lodash merge-এর অনেক পুরোনো সংস্করণ ঠিক এজন্যই রিপোর্ট হয়েছিল।

১.৪ পাথ দিয়ে অ্যাসাইনমেন্ট সমান বিপজ্জনক

// Anti-pattern: the path comes from user input
for (const segment of pathSegments) {
  current = current[segment] ?? (current[segment] = {});
}

pathSegments-এ __proto__ এলে এটি উপরের দূষণেরই সমান। JSON Pointer / JSON Patch-এর বাস্তবায়নে প্রতিটি টোকেন সেগমেন্ট যাচাই করতে হবে।

২. রিসোর্স নিঃশেষ আক্রমণ

আক্রমণ কৌশল প্রশমন
গভীর নেস্টিং এক লক্ষ স্তরের [[[[...]]]] নেস্টিং গভীরতা সীমিত করুন (সাধারণত ৩২–৬৪) আর পার্সের আগে প্রি-স্ক্যান করুন
অতিরিক্ত বড় অ্যারে কোটি কোটি এলিমেন্টের অ্যারে রিকোয়েস্ট বডির আকার ও অ্যারের দৈর্ঘ্য সীমিত করুন
অত্যন্ত দীর্ঘ স্ট্রিং শত শত MB-র একক স্ট্রিং ফিল্ডের দৈর্ঘ্য সীমিত করুন
ডুপ্লিকেট-কি বিস্ফোরণ বিপুল সংখ্যক ডুপ্লিকেট কি অবজেক্টের কি-সংখ্যা সীমিত করুন

সার্ভারে সবচেয়ে কার্যকর দুই প্রতিরক্ষা রিকোয়েস্ট বডির আকার সীমা ও গভীরতার সীমা, আর দুটিই যেকোনো পার্সিংয়ের আগে কার্যকর হতে হবে।

/**
 * 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;
}

৩. HTML-এ ইনলাইন করার সময় ইনজেকশন

JSON সোজা পেজে লেখা একটি প্রচলিত অভ্যাস, কিন্তু এটি তিন শ্রেণির সমস্যা আনে:

<script>
  window.__DATA__ = {"title": "</script><script>alert(1)</script>"};
</script>

স্ট্রিংয়ের ভেতরের </script> স্ক্রিপ্ট ব্লক আগেই বন্ধ করে দেয়, আর বাকিটা HTML হিসেবে পার্স হয় — ক্লাসিক XSS।

আরও দুই অক্ষর একই আচরণ করে: \u2028 (লাইন সেপারেটর) ও \u2029 (প্যারাগ্রাফ সেপারেটর) JSON-এ বৈধ, কিন্তু JS সোর্সে এম্বেড করলে সিনট্যাক্স এরর ঘটায়।

সমাধান:

/**
 * 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');
}

আরও মজবুত পথ হলো ডেটা <script type="application/json">-এর ভেতরে রাখা (যার কন্টেন্ট চালানো হয় না), তারপর JSON.parse(document.getElementById('data').textContent) দিয়ে পড়া।

৪. JSON হাইজ্যাকিং ও JSONP

প্রাথমিক ব্রাউজারগুলো ক্রস-অরিজিন <script src="https://api.example.com/user">-কে ফেরত আসা JavaScript সরাসরি চালাতে দিত, আর টপ লেভেল অ্যারে হওয়া JSON দৈবক্রমে বৈধ JS এক্সপ্রেশনও, তাই আক্রমণকারী Array কনস্ট্রাক্টর ওভাররাইড করে ডেটা পড়তে পারত।

আধুনিক ব্রাউজারে প্রতিরক্ষা আছে, তবু আমরা সুপারিশ করি:

  • API-এর টপ লেভেলে অ্যারের বদলে অবজেক্ট ব্যবহার করুন ({"data": [...]});
  • X-Content-Type-Options: nosniff পাঠান আর Content-Type: application/json কঠোরভাবে সেট করুন;
  • JSONP ব্যবহার বন্ধ করুন; ক্রস-অরিজিন অ্যাক্সেস সত্যিই দরকার হলে CORS ব্যবহার করুন।

৫. অবজেক্টে ডিসিরিয়ালাইজ করার সময় টাইপ কনফিউশন

Jackson-এ ডিফল্ট টাইপিং চালু করলে (enableDefaultTyping), আক্রমণকারী একটি @class ফিল্ড দিয়ে সার্ভারকে যেকোনো ক্লাস ইনস্ট্যানশিয়েট করাতে পারে, আর শোষণযোগ্য gadget chain-এর সঙ্গে মিলিয়ে রিমোট কোড এক্সিকিউশন ঘটাতে পারে। এ ধরনের দুর্বলতা বহুবার এসেছে (Fastjson ও Jackson — দুটোই আক্রান্ত হয়েছে)।

নীতি: ডিসিরিয়ালাইজেশনের লক্ষ্য টাইপ সার্ভার কোডে স্থিরভাবে নির্ধারিত হতে হবে আর কখনো পেললোডের কন্টেন্ট দিয়ে ঠিক হবে না।

৬. রেগুলার এক্সপ্রেশন ও স্কিমায় ডিনায়াল অফ সার্ভিস

  • JSON Schema-র pattern ও JSONPath-এর match() — দুটোই রেগুলার এক্সপ্রেশন গ্রহণ করে;
  • (a+)+$-এর মতো প্যাটার্ন দীর্ঘ ইনপুটে সূচকীয়ভাবে ব্যাকট্র্যাক করে;
  • রেগুলার এক্সপ্রেশন ব্যবহারকারীর কাছ থেকে এলে (কাস্টম ভ্যালিডেশন নিয়ম), এর দৈর্ঘ্য সীমিত করুন, ব্যাকরেফারেন্স ও নেস্টেড কোয়ান্টিফায়ার নিষিদ্ধ করুন, বা RE2-এর মতো রৈখিক ইঞ্জিনে যান।

৭. লগিং ও মাস্কিং

JSON লগ সংবেদনশীল তথ্য ফাঁসের সবচেয়ে সহজ পথ:

/** 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;
}

লক্ষ্য করুন মাস্কিং লগ লেখার আগেই হতে হবে, পরে লগিং প্ল্যাটফর্ম পরিষ্কার করে নয়।

৮. প্রকাশের আগের চেকলিস্ট

  • [ ] সব গভীর-মার্জ ফাংশন __proto__ / prototype / constructor প্রত্যাখ্যান করে;
  • [ ] JSON Pointer / Patch-এর পাথ টোকেন হোয়াইটলিস্ট দিয়ে যাচাই হয়;
  • [ ] রিকোয়েস্ট বডির আকার ও নেস্টিং গভীরতার কঠোর সীমা আছে, আর সেই সীমা পার্সিংয়ের আগেই কার্যকর;
  • [ ] HTML-এ ইনলাইন করা JSON <, >, &, U+2028 ও U+2029 এস্কেপ করে;
  • [ ] API টপ লেভেলে অবজেক্ট ব্যবহার করে আর রেসপন্স nosniff পাঠায়;
  • [ ] ডিসিরিয়ালাইজেশনের লক্ষ্য টাইপ কোডে স্থিরভাবে নির্ধারিত;
  • [ ] ব্যবহারকারী-নিয়ন্ত্রিত রেগুলার এক্সপ্রেশনের জটিলতা সীমা আছে;
  • [ ] লগ ফিল্ডের মাস্কিং নিয়ম আছে আর সেই নিয়ম পরীক্ষিত।

সাধারণ প্রশ্ন

আমার গভীর মার্জ প্রোটোটাইপ দূষণে আক্রান্ত কি না কীভাবে যাচাই করব?

মার্জ ফাংশনে JSON.parse('{"__proto__": {"isAdmin": true}}') ঢুকিয়ে দেখুন {}.isAdmin true হয়ে যায় কি না; constructor.prototype পেললোড দিয়ে পুনরাবৃত্তি করুন। তৃতীয়-পক্ষ লাইব্রেরিতে নিশ্চিত করুন এটি __proto__, prototype ও constructor প্রত্যাখ্যান করে — lodash merge-এর অনেক পুরোনো সংস্করণ ঠিক এজন্যই রিপোর্ট হয়েছিল। পাথ-ভিত্তিক অ্যাসাইনমেন্টের আলাদা যাচাই দরকার, কারণ পাথে __proto__ টোকেন একইভাবে দূষিত করে।

JSON.parse নিজে প্রোটোটাইপ দূষণের উৎস নয় কেন?

এটি __proto__-কে সাধারণ নিজস্ব প্রপার্টি হিসেবে তৈরি করে আর প্রোটোটাইপ চেইন কখনো আবার লেখে না, তাই data.polluted ও {}.polluted — দুটোই undefined থাকে। দূষণ পরে ঘটে: অসুরক্ষিত গভীর মার্জ উৎস অবজেক্টের কি সোজা লক্ষ্যে লেখে, আর constructor.prototype একই ধরনের প্রবেশবিন্দু। JSON.parse-কে মূল কারণ ধরলে ভুল জায়গা ঠিক করা হয়।

HTML-এ ইনলাইন করা JSON কি সবসময় এস্কেপ করা উচিত?

হ্যাঁ। স্ট্রিংয়ের ভেতরের </script> স্ক্রিপ্ট ব্লক আগেই বন্ধ করে বাকিটা HTML হিসেবে পার্স হয় — ক্লাসিক XSS; <, >, &, U+2028 ও U+2029-ও এস্কেপ দরকার। নিরাপদ বিকল্প হলো ডেটা <script type="application/json">-এ রাখা, যার কন্টেন্ট চালানো হয় না, আর textContent-এ JSON.parse দিয়ে পড়া।