JS `JSON.parse` 的安全陷阱与 `JSON.stringify` 的循环引用处理

咳咳,大家好!我是今天的主讲人,咱们今天来聊聊JSON这个看似简单,实则暗藏玄机的家伙。别看它经常抛头露面,在前端后端之间穿梭,一不小心,它也会给你挖个坑。今天我们就重点说说 JSON.parse 的安全隐患,以及 JSON.stringify 如何处理循环引用。

开场白:JSON,你以为你很了解它吗?

JSON (JavaScript Object Notation),是一种轻量级的数据交换格式。它易于人阅读和编写,同时也易于机器解析和生成。由于它简洁明了,所以被广泛应用于Web应用中。但是,JSON并非完美无缺,尤其是在处理用户输入或复杂数据结构时,稍有不慎就会遇到安全问题或程序崩溃。

第一幕:JSON.parse 的安全陷阱:别轻信你收到的“糖衣炮弹”

JSON.parse 函数用于将一个 JSON 字符串转换为 JavaScript 对象。这听起来很简单,但其中隐藏着一些安全风险。

  • 陷阱一:原型污染 (Prototype Pollution)

    原型污染是一种攻击方式,攻击者通过修改 JavaScript 对象的原型,来影响所有继承自该原型的对象。JSON.parse 本身并没有直接导致原型污染,但是当和一些库或者框架不安全的使用方式结合起来,就可能产生问题。

    考虑以下情况:

    // 假设我们从服务端收到以下JSON数据
    const maliciousJSON = '{"__proto__": {"isAdmin": true}}';
    
    // 将JSON字符串转换为JavaScript对象
    JSON.parse(maliciousJSON);
    
    // 检查所有对象的原型链上是否都有 isAdmin 属性
    console.log(({}).isAdmin); // 输出 true

    在这个例子中,攻击者通过控制 JSON 字符串,修改了 Object.prototype,导致所有对象都继承了 isAdmin 属性,并且值为 true。这意味着任何对象,无论是否应该拥有管理员权限,都会被认为是管理员。

    如何防范:

    • 不要信任所有外部数据。 对从网络接收到的 JSON 数据进行严格的验证和过滤。

    • 使用 Object.create(null) 创建无原型对象。 如果你需要一个纯粹的键值对存储,可以使用 Object.create(null) 创建一个没有原型的对象。这样,即使攻击者试图修改 __proto__,也不会影响到其他对象。

      const maliciousJSON = '{"__proto__": {"isAdmin": true}}';
      const parsedObject = JSON.parse(maliciousJSON);
      
      const safeObject = Object.create(null);
      Object.assign(safeObject, parsedObject); // 将解析后的对象属性复制到安全对象中
      
      console.log(safeObject.isAdmin); // 输出 undefined
      console.log(({}).isAdmin); // 输出 undefined
    • 使用 Object.freezeObject.seal 冻结对象。 Object.freeze 可以防止对象被修改,Object.seal 可以防止对象添加新属性。

      const safeObject = { name: "user" };
      Object.freeze(safeObject);
      
      try {
        safeObject.name = "admin"; // 尝试修改会抛出 TypeError (严格模式下)
      } catch (e) {
        console.error(e);
      }
      
      console.log(safeObject.name); // 输出 "user"
  • 陷阱二:代码注入 (Code Injection)

    虽然 JSON.parse 本身不会直接执行代码,但在某些情况下,它可能被用来间接执行代码。这通常发生在与 eval() 函数或类似的动态代码执行机制结合使用时。

    const maliciousJSON = '{"constructor": {"prototype": {"isAdmin": "alert('Hacked!')"}}}';
    
    //  不安全的代码!千万不要这样做!
    // eval(`(${JSON.parse(maliciousJSON)})`); //  执行恶意代码

    在这个例子中,攻击者通过控制 JSON 字符串,修改了 constructor.prototype,试图注入恶意代码。如果使用了 eval() 函数,这段代码就会被执行,导致安全问题。

    如何防范:

    • 永远不要使用 eval() 函数或类似的动态代码执行机制来处理 JSON 数据。 这是最重要的一点!
    • 使用安全的解析方法。 JSON.parse 是相对安全的,只要你不将解析后的数据传递给不安全的函数。
  • 陷阱三:拒绝服务 (Denial of Service, DoS)

    攻击者可以发送包含大量嵌套或重复键的 JSON 数据,导致 JSON.parse 消耗大量资源,从而使服务器崩溃或响应缓慢。

    //  构造一个包含大量嵌套的 JSON 字符串
    let maliciousJSON = '{"a":';
    for (let i = 0; i < 10000; i++) {
      maliciousJSON += '{"a":';
    }
    maliciousJSON += '"b"';
    for (let i = 0; i < 10000; i++) {
      maliciousJSON += '}';
    }
    
    //  尝试解析这个 JSON 字符串
    try {
      JSON.parse(maliciousJSON);
    } catch (e) {
      console.error(e); //  可能会抛出 RangeError: Maximum call stack size exceeded
    }

    如何防范:

    • 限制 JSON 数据的大小。 在服务器端设置 JSON 数据的大小限制,防止攻击者发送过大的 JSON 数据。
    • 使用超时机制。 在解析 JSON 数据时设置超时时间,如果超过时间限制,则停止解析,防止资源被耗尽。
    • 使用流式解析器。 流式解析器可以逐步解析 JSON 数据,而不是一次性加载所有数据,从而降低内存消耗。

第二幕:JSON.stringify 的循环引用处理:剪不断,理还乱?

JSON.stringify 函数用于将 JavaScript 对象转换为 JSON 字符串。当对象包含循环引用时,JSON.stringify 会抛出一个 TypeError 错误。

  • 什么是循环引用?

    循环引用是指对象之间相互引用,形成一个闭环。例如:

    const obj1 = {};
    const obj2 = {};
    
    obj1.prop = obj2;
    obj2.prop = obj1; // 循环引用

    在这个例子中,obj1prop 属性引用了 obj2,而 obj2prop 属性又引用了 obj1,形成了一个循环。

  • JSON.stringify 如何处理循环引用?

    默认情况下,JSON.stringify 检测到循环引用会抛出 TypeError: Converting circular structure to JSON 错误。

    const obj1 = {};
    const obj2 = {};
    
    obj1.prop = obj2;
    obj2.prop = obj1;
    
    try {
      JSON.stringify(obj1); // 抛出 TypeError
    } catch (e) {
      console.error(e);
    }
  • 如何解决循环引用问题?

    JSON.stringify 提供了一个 replacer 参数,可以用来控制哪些属性应该被序列化。我们可以使用 replacer 函数来检测循环引用,并将其替换为 undefined 或其他安全的值。

    const obj1 = {};
    const obj2 = {};
    
    obj1.prop = obj2;
    obj2.prop = obj1;
    
    const seen = new WeakSet(); // 使用 WeakSet 存储已经遍历过的对象
    
    const replacer = (key, value) => {
      if (typeof value === "object" && value !== null) {
        if (seen.has(value)) {
          return undefined; // 移除循环引用
        }
        seen.add(value);
      }
      return value;
    };
    
    const jsonString = JSON.stringify(obj1, replacer);
    console.log(jsonString); // 输出 {"prop":{}}

    在这个例子中,我们使用了一个 WeakSet 来存储已经遍历过的对象。当 replacer 函数遇到一个对象时,它会检查该对象是否已经在 WeakSet 中。如果在,说明存在循环引用,将其替换为 undefined

    其他方法:

    • 手动断开循环引用。 在序列化之前,手动将循环引用属性设置为 nullundefined
    • 使用第三方库。 一些第三方库提供了更高级的循环引用处理功能,例如 cycle.js
  • replacer 函数详解

    replacer 函数是一个可选参数,传递给 JSON.stringify 函数,用于转换对象的属性。它可以是函数或数组。

    • replacer 函数:

      replacer 函数接收两个参数:keyvaluekey 是属性名(字符串),value 是属性值。函数应该返回最终要添加到 JSON 字符串中的值。如果返回 undefined,则该属性会被忽略。

      const obj = {
        name: "Alice",
        age: 30,
        city: "New York"
      };
      
      const replacer = (key, value) => {
        if (key === "age") {
          return undefined; //  忽略 age 属性
        }
        return value;
      };
      
      const jsonString = JSON.stringify(obj, replacer);
      console.log(jsonString); // 输出 {"name":"Alice","city":"New York"}
    • replacer 数组:

      replacer 数组是一个包含属性名的字符串数组。只有出现在数组中的属性才会被序列化。

      const obj = {
        name: "Alice",
        age: 30,
        city: "New York"
      };
      
      const replacer = ["name", "city"]; //  只序列化 name 和 city 属性
      
      const jsonString = JSON.stringify(obj, replacer);
      console.log(jsonString); // 输出 {"name":"Alice","city":"New York"}

第三幕:总结与建议

JSON 虽然方便易用,但安全问题不容忽视。在使用 JSON.parse 时,要警惕原型污染、代码注入和拒绝服务攻击。在使用 JSON.stringify 时,要妥善处理循环引用问题。

问题 解决方案
原型污染 不要信任外部数据,使用 Object.create(null) 创建无原型对象,使用 Object.freezeObject.seal 冻结对象。
代码注入 永远不要使用 eval() 函数或类似的动态代码执行机制来处理 JSON 数据。
拒绝服务 限制 JSON 数据的大小,使用超时机制,使用流式解析器。
循环引用 使用 replacer 函数检测循环引用,并将其替换为 undefined 或其他安全的值,手动断开循环引用,使用第三方库。

最后,请记住:

  • 安全第一! 在处理 JSON 数据时,始终要考虑安全问题。
  • 验证输入! 对所有外部数据进行严格的验证和过滤。
  • 使用安全的 API! 避免使用不安全的 API,例如 eval() 函数。
  • 保持警惕! 随时关注新的安全漏洞和攻击方式。

好了,今天的讲座就到这里。希望大家以后在使用 JSON 时,能够更加小心谨慎,避免踩坑。下次有机会再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注