安全设置未知属性(缓解方括号对象注入攻击)实用程序功能

Dev*_*ode 10 javascript security code-injection square-bracket

设置好之后eslint-plugin-security,我继续尝试解决我们javascript代码库中方括号的近400种用法(由安全性/检测对象注入规则标记)。尽管此插件可能更智能,但方括号的任何使用都可能成为恶意代理注入自己的代码的机会。

要了解如何理解问题的整个上下文,您需要阅读以下文档:https : //github.com/nodesecurity/eslint-plugin-security/blob/master/docs/the-dangers-of-square -bracket-notation.md

我通常尝试使用Object.prototype.hasOwnProperty.call(someObject, someProperty)可以减轻someProperty恶意设置的机会的方法constructor。许多情况只是在for循环(for (let i=0;i<arr.length;i++) { arr[i] })中取消引用数组索引。如果i始终为int,则显然始终是安全的。

我认为我无法完美处理的一种情况是如下方括号分配

someObject[somePropertyPotentiallyDefinedFromBackend] = someStringPotentiallyMaliciouslyDefinedString
Run Code Online (Sandbox Code Playgroud)

StackOverflow的现状是“向我展示所有代码”-当您遍历一个代码库并在成百上千个实例中修复它们时,花很长时间才能阅读这些任务之一之前的代码。 。此外,我们希望确保此代码在将来进行修改时保持安全。

我们如何确保要设置的属性在香草对象上基本上尚未定义?(即constructor

尝试自己解决此问题,但缺少一些碎片。最终将对其进行编辑,但留在此处以供参考。

因此,我认为解决此问题的最简单方法是使用一个简单的util,safeKey定义如下:

// use window.safeKey = for easy tinkering in the console.
const safeKey = (() => {
  // Safely allocate plainObject's inside iife
  // Since this function may get called very frequently -
  // I think it's important to have plainObject's
  // statically defined
  const obj = {};
  const arr = [];
  // ...if for some reason you ever use square brackets on these types...
  // const fun = function() {}
  // const bol = true;
  // const num = 0;
  // const str = '';
  return key => {
    // eslint-disable-next-line security/detect-object-injection
    if (obj[key] !== undefined || arr[key] !== undefined
      // ||
      // fun[key] !== undefined ||
      // bol[key] !== undefined ||
      // num[key] !== undefined ||
      // str[key] !== undefined
    ) {
      return 'SAFE_'+key;
    } else {
      return key;
    }
  };
})();
Run Code Online (Sandbox Code Playgroud)

我们还可以编写一个util- safeSet代替:

obj[key] = value;
Run Code Online (Sandbox Code Playgroud)

您可以这样做:

safeSet(obj, key, value)
Run Code Online (Sandbox Code Playgroud)

测试safeKey(失败):

console.log(safeKey('toString'));
//     Good: => SAFE_toString

console.log(safeKey('__proto__'));
//     Good: => SAFE___proto__

console.log(safeKey('constructor'));
//     Good: => SAFE_constructor

console.log(safeKey('prototype'));
//     Fail: =>      prototype

console.log(safeKey('toJSON'));
//     Fail: =>      toJSON
Run Code Online (Sandbox Code Playgroud)

然后,您可以像这样使用它:

someObject[safeKey(somePropertyPotentiallyDefinedFromBackend)] = someStringPotentiallyMaliciouslyDefinedString
Run Code Online (Sandbox Code Playgroud)

这意味着,如果后端偶然向constructor我们发送带有密钥的json,而我们对此并不感到厌烦,而是仅使用密钥SAFE_constructor(lol)。也适用于任何其他预定义的方法/属性,因此现在后端不必担心json键与本机定义的js属性/方法发生冲突。

如果没有一系列通过的单元测试,此util函数将毫无用处。正如我所评论的,并非所有测试都通过了。我不确定哪个对象本机定义为JSON-这意味着它可能需要成为必须列入黑名单的方法/属性名称的硬编码列表的一部分。但是我不确定如何找出这些需要列入黑名单的属性方法中的一种。因此,我们需要知道任何人可以生成此列表并保持更新的最佳方式。

我确实发现使用Object.freeze(Object.prototype)会有所帮助,但是原型中并不存在像toJSON这样的方法。

这是另一个小测试用例:

const prop = 'toString';
someData[safeKey(prop)] = () => {
    alert('hacked');
    return 'foo';
};
console.log('someProp.toString()', someData + '');
Run Code Online (Sandbox Code Playgroud)

相关:评估javascript字符串的所有方法:https : //www.everythingfrontend.com/posts/studying-javascript-eval.html 我向推特发送了推文,提到了constructor漏洞。

use*_*170 9

防止密钥在错误的对象上被访问比验证/保护对象密钥本身更重要。将某些对象键指定为“不安全”并避免在任何情况下只访问这些键只是“消毒”反模式的另一种形式。如果对象首先不包含敏感数据,则不存在被不可信输入窃取或修改的风险。您无需担心访问srcinnerHTML不在 DOM 节点上访问它;eval如果不对全局对象执行查找,则无需担心暴露。像这样:

  • 仅对数组或特别包含从任意字符串到其他值的映射(通常由对象字面量表示法构造的值)的映射的对象使用方括号表示法;这种对象我将在下面称为类似地图的对象。使用类似地图的对象时,还要确保以下几点:
    • 你永远不会将函数(或者,在 ECMAScript 的更高版本中,类或代理)直接存储在类似地图的对象中。这是为了避免当键喜欢'toJSON''then'映射到语言可能会解释为修改对象行为的方法的函数时出现问题。如果出于某种原因,您需要将函数存储在类似地图的对象中,请将函数放在类似{ _: function () { /* ... */ } }.
    • 永远不要使用内置语言机制将类似映射的对象强制转换为字符串:toString方法、String构造函数(带或不带new)、+运算符或Array.prototype.join。这是为了避免'toString'在类似地图的对象上设置键时触发问题,因为即使是非函数值也会阻止默认强制行为的发生,而是会抛出TypeError.
  • 访问数组时,请确保索引确实是整数。还可以考虑使用诸如push, 之类的内置方法forEachmap或者filter完全避免显式索引;这将减少您需要审核的地方数量。
  • 如果您需要将任意数据与具有一组相对固定键的对象相关联,例如 DOM 节点window或您定义的对象class(所有这些我将在下面称为类类),并且由于某种原因WeakMap不是可用,将数据放在硬编码的密钥上;如果您有多个这样的数据项,请将其放入存储在硬编码键上的类似地图的对象中。

即使按照上述操作,您仍然可能因无意中访问Object.prototype. 特别令人担忧的是 constructor 各种内置方法(可用于访问Function对象,并最终执行任意代码执行)和__proto__(可用于修改对象的原型链)。为了防范这些威胁,您可以尝试以下一些策略。它们并不相互排斥,但为了一致性起见,最好只使用一个。

  • Mangle all keys:这可能是(概念上)最简单的选项,甚至可以移植到 ECMAScript 3 时代的引擎,并且即使将来添加Object.prototype(尽管不太可能)也很健壮。只需在类地图对象中的所有键前添加一个非标识符字符即可;这将安全地从所有可以合理想象的 JavaScript 内置函数(大概应该具有有效标识符的名称)中安全地命名空间远离不受信任的键。访问类似地图的对象时,请检查此字符并根据需要对其进行剥离。遵循此策略甚至会使对类似toJSONtoString几乎不相关的方法的担忧。

    // replacement for (key in maplike)
    function maplikeContains(maplike, key) {
        return ('.' + key) in maplike;
    }
    
    // replacement for (maplike[key])
    function maplikeGet(maplike, key) {
        return maplike['.' + key];
    }
    
    // replacement for (maplike[key] = value)
    function maplikeSet(maplike, key, value) {
        return maplike['.' + key] = value;
    }
    
    // replacement for the for-in loop
    function maplikeEach(maplike, visit) {
        for (var key in maplike) {
            if (key.charAt(0) !== '.')
                continue;
            if (visit(key.substr(1), maplike[key]))
                break;
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    重整所有按键胡乱确保您不会结束了混乱的错位键或反之亦然未重整键。例如,如果像在问题中一样,您修改'constructor''SAFE_constructor',但保持'SAFE_constructor'原样,那么在修改两个密钥后最终将引用相同的数据,这本身可能是一个安全漏洞。

    这种方法的一个缺点是前缀字符将在 JSON 中结束,如果你曾经序列化过这样一个类似地图的对象。

  • 强制直接访问属性。可以使用 来保护读取访问Object.prototype.hasOwnProperty,这将阻止渗漏漏洞,但不会防止您无意中写入__proto__. 如果你从不改变这样一个类似地图的对象,这应该不是问题。您甚至可以使用Object.seal. 如果不想这样,您可以通过 执行属性写入Object.defineProperty,自 ECMAScript 5 起可用,它可以直接在对象上创建属性,绕过 getter 和 setter。

    // replacement for (key in maplike)
    function maplikeContains(maplike, key) {
        return Object.prototype.hasOwnProperty.call(maplike, key);
    }
    
    // replacement for (maplike[key])
    function maplikeGet(maplike, key) {
        if (Object.prototype.hasOwnProperty.call(maplike, key))
            return maplike[key];
    }
    
    // replacement for (maplike[key] = value)
    function maplikeSet(maplike, key, value) {
        Object.defineProperty(maplike, key, {
            value: value,
            writable: true,
            enumerable: true,
            configurable: true
        });
        return value;
    }
    
    // replacement for the for-in loop
    function maplikeEach(maplike, visit) {
        for (var key in maplike) {
            if (!Object.prototype.hasOwnProperty.call(maplike, key))
                continue;
            if (visit(key, maplike[key]))
                break;
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • 清除原型链:确保类似地图的对象有一个空的原型链。通过Object.create(null)(自 ECMAScript 5 起可用)而不是{}. 如果您之前通过直接对象字面量创建它们,您可以将它们包装在Object.assign(Object.create(null), { /* ... */ })Object.assign自 ECMAScript 6 起可用,但很容易与早期版本兼容)。如果你遵循这种方法,你可以像往常一样使用括号表示法;您需要检查的唯一代码是您构造类似地图的对象的位置。

    JSON.parse默认情况下,由will创建的对象仍将继承自Object.prototype(尽管现代引擎至少会__proto__直接在构造的对象本身上添加一个 JSON 键,绕过原型描述符中的 setter)。您可以将此类对象视为只读对象,并通过 hasOwnProperty (如上所述)保护读取访问,或者通过编写调用Object.setPrototypeOf. reviver 函数也可以Object.seal用来使对象不可变。

    function maplikeNew(maplike) {
        return Object.assign(Object.create(null), maplike);
    }
    
    function jsonParse(json) {
        return JSON.parse(json, function (key, value) {
            if (typeof value === 'object' && value !== null && !Array.isArray(value))
                Object.setPrototypeOf(value, null);
            return value;
        });
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • 使用Maps而不是类似地图的对象:使用Map(自 ECMAScript 6 起可用)允许您使用字符串以外的键,这对于普通对象是不可能的;但即使只是使用字符串键,您也可以享受地图条目与地图对象本身的原型链完全隔离的好处。Maps 中的项目由.get.set方法而不是括号表示法访问,并且根本不会与属性冲突:键存在于单独的命名空间中。

    但是存在Map无法直接序列化为 JSON 的问题。您可以通过编写一个替换函数来解决这个问题,该函数JSON.stringifyMaps转换为普通的、无原型的类似映射的对象,以及一个JSON.parse将普通对象转换回Maps的 reviver 函数。再说一次,天真地将每个JSON 对象恢复为 aMap也将涵盖我在上面称为“类类”的结构,您可能不想要这种结构。要区分它们,您可能需要向 JSON 解析函数添加某种架构参数。

    function jsonParse(json) {
        return JSON.parse(json, function (key, value) {
            if (typeof value === 'object' && value !== null && !Array.isArray(value))
                return new Map(Object.entries(value));
            return value;
        });
    }
    
    function jsonStringify(value) {
        return JSON.stringify(value, function (key, value) {
            if (value instanceof Map)
                return Object.fromEntries(value.entries());
            return value;
        });
    }
    
    Run Code Online (Sandbox Code Playgroud)

如果您问我的偏好:Map如果您不需要担心 ES6 之前的引擎或 JSON 序列化,请使用;否则使用Object.create(null);如果您需要使用两种都不可能的遗留 JS 引擎,请修改键(第一个选项)并希望最好。

现在,所有这些纪律都可以机械地执行吗?是的,它被称为静态类型。有了足够好的类型定义,TypeScript 应该能够捕获以类映射方式访问类类对象的情况,反之亦然。它甚至可以捕获出现具有不需要的原型的对象的某些情况:

// replacement for (key in maplike)
function maplikeContains(maplike, key) {
    return ('.' + key) in maplike;
}

// replacement for (maplike[key])
function maplikeGet(maplike, key) {
    return maplike['.' + key];
}

// replacement for (maplike[key] = value)
function maplikeSet(maplike, key, value) {
    return maplike['.' + key] = value;
}

// replacement for the for-in loop
function maplikeEach(maplike, visit) {
    for (var key in maplike) {
        if (key.charAt(0) !== '.')
            continue;
        if (visit(key.substr(1), maplike[key]))
            break;
    }
}
Run Code Online (Sandbox Code Playgroud)

但是请记住,这不是万能药。上面的类型定义可能会捕捉到最明显的错误,但是想出一个它不会检测到的情况并不难。