繁体   English   中英

如何减轻 JavaScript 中动态属性访问(即方括号表示法)的注入/渗漏攻击?

[英]How can I mitigate injection/exfiltration attacks from dynamic property accesses (i.e. square bracket notation) in JavaScript?

在设置eslint-plugin-security ,我继续尝试解决我们 JavaScript 代码库中近 400 次方括号的使用(由规则 security/detect-object-injection 标记)。 虽然这个插件可以更智能,但任何方括号的使用都可能成为恶意代理注入自己代码的机会。

要了解如何以及了解我的问题的整个上下文,您需要阅读此文档: 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总是一个数字,这显然总是安全的。

我认为我没有完美处理的一种情况是这样的方括号分配

someObject[somePropertyPotentiallyDefinedFromBackend] = someStringPotentiallyMaliciouslyDefinedString

我认为解决这个问题的最简单方法是使用一个简单的 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;
    }
  };
})();

然后你会像这样使用它:

someObject[safeKey(somePropertyPotentiallyDefinedFromBackend)] = someStringPotentiallyMaliciouslyDefinedString

这意味着如果后端偶然发送带有constructor某处密钥的 JSON,我们不会阻塞它,而只是使用密钥SAFE_constructor (lol)。 也适用于任何其他预定义的方法/属性,因此现在后端不必担心 JSON 键与本机定义的 JS 属性/方法发生冲突。

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

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

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

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

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

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

  • Mangle all keys :这可能是(概念上)最简单的选项,甚至可以移植到 ECMAScript 3 时代的引擎,并且即使将来添加到Object.prototype也很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; } }

    不加选择地修改所有键可确保您最终不会将未修改的键与损坏的键混淆,反之亦然。 例如,如果像在问题中一样,您将'constructor''SAFE_constructor' ,但将'SAFE_constructor'本身保留'SAFE_constructor' ,那么在修改两个键后最终将引用相同的数据,这可能是本身。

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

  • 强制直接访问属性 可以使用Object.prototype.hasOwnProperty保护读取访问,这将阻止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; } }
  • 清除原型链:确保类似地图的对象有一个空的原型链。 通过Object.create(null) (从 ECMAScript 5 开始可用)而不是{}创建它们。 如果您之前通过直接对象字面量创建它们,您可以将它们包装在Object.assign(Object.create(null), { /* ... */ })Object.assign自 ECMAScript 6 起可用,但很容易 shimmable早期版本)。 如果你遵循这种方法,你可以像往常一样使用括号表示法; 您需要检查的唯一代码是您构造类似地图的对象的位置。

    默认情况下,由JSON.parse创建的对象仍将继承自Object.prototype (尽管现代引擎至少会直接在构造的对象本身上添加像__proto__这样的 JSON 键,绕过原型描述符中的 setter)。 您可以将此类对象视为只读对象,并通过hasOwnProperty (如上所述)保护读取访问,或者通过编写调用Object.setPrototypeOf的 reviver 函数来剥离它们的原型。 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; }); }
  • 使用Map代替类似地图的对象:使用Map (自 ECMAScript 6 起可用)允许您使用字符串以外的键,这对于普通对象是不可能的; 但即使只是使用字符串键,您也可以享受地图条目与地图对象本身的原型链完全隔离的好处。 Map的项目通过.get.set方法而不是括号表示法访问,并且根本不会与属性冲突:键存在于单独的命名空间中。

    但是有一个问题,一个Map不能直接序列化成 JSON。 您可以通过为JSON.stringify编写一个JSON.stringify函数来解决这个问题,该函数将Map s 转换为普通的、无原型的类似 map 的对象,并为JSON.parse编写一个 reviver 函数,将普通对象转换回Map s。 再说一次,将每个JSON 对象天真地恢复为Map也将涵盖我在上面称为“类类”的结构,您可能不想要这种结构。 要区分它们,您可能需要向 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; }); }

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

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

type Maplike<T> = {
    [K: string]: T|undefined;
    constructor?: T;
    propertyIsEnumerable?: T;
    hasOwnProperty?: T;
    toString?: T;
    toLocaleString?: T;
    valueOf?: T;
};

const plain: { [K: string]: string } = {};

const naked: Maplike<string> = Object.create(null);   // OK
const error: Maplike<string> = {};                    // type error

function f(k: string) {
    naked[k] = 'yay';                                 // OK
    plain[k] = 'yay';                                 // OK
    document.body[k] = 'yay';                         // type error
}

console.info(plain.toString());                       // OK
console.info(naked.toString());                       // type error

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM