简体   繁体   English

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

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

After setting up eslint-plugin-security , I went on to attempt to address nearly 400 uses of square brackets in our JavaScript codebase (flagged by the rule security/detect-object-injection).在设置eslint-plugin-security ,我继续尝试解决我们 JavaScript 代码库中近 400 次方括号的使用(由规则 security/detect-object-injection 标记)。 Although this plugin could be a lot more intelligent, any uses of square brackets could possibly be an opportunity for a malicious agent to inject their own code.虽然这个插件可以更智能,但任何方括号的使用都可能成为恶意代理注入自己代码的机会。

To understand how, and to understand the whole context of my question, you need to read this documentation: https://github.com/nodesecurity/eslint-plugin-security/blob/master/docs/the-dangers-of-square-bracket-notation.md要了解如何以及了解我的问题的整个上下文,您需要阅读此文档: https : //github.com/nodesecurity/eslint-plugin-security/blob/master/docs/the-dangers-of-square -bracket-notation.md

I generally tried to use Object.prototype.hasOwnProperty.call(someObject, someProperty) where I could to mitigate the chance that someProperty is maliciously set to constructor .我通常尝试使用Object.prototype.hasOwnProperty.call(someObject, someProperty)来减少someProperty被恶意设置为constructor的可能性。 Lot of situations were simply dereferencing an array index in for loops ( for (let i=0;i<arr.length;i++) { arr[i] } ) If i is always a number, this is obviously always safe.很多情况只是在 for 循环中取消引用数组索引( for (let i=0;i<arr.length;i++) { arr[i] } )如果i总是一个数字,这显然总是安全的。

One situation I don't think I have handled perfectly, are square bracket assignments like this:我认为我没有完美处理的一种情况是这样的方括号分配

someObject[somePropertyPotentiallyDefinedFromBackend] = someStringPotentiallyMaliciouslyDefinedString

I think the easiest way to solve this issue is with a simple util, safeKey defined as such:我认为解决这个问题的最简单方法是使用一个简单的 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;
    }
  };
})();

You'd then use it like so:然后你会像这样使用它:

someObject[safeKey(somePropertyPotentiallyDefinedFromBackend)] = someStringPotentiallyMaliciouslyDefinedString

This means if the backend incidentally sends JSON with a key somewhere of constructor we don't choke on it, and instead just use the key SAFE_constructor (lol).这意味着如果后端偶然发送带有constructor某处密钥的 JSON,我们不会阻塞它,而只是使用密钥SAFE_constructor (lol)。 Also applies for any other pre-defined method/property, so now the backend doesn't have to worry about JSON keys colliding with natively defined JS properties/methods.也适用于任何其他预定义的方法/属性,因此现在后端不必担心 JSON 键与本机定义的 JS 属性/方法发生冲突。

This utility function is nothing without a series of passing unit tests.如果没有一系列通过的单元测试,这个实用函数就毫无意义。 As I've commented not all the tests are passing.正如我所评论的,并不是所有的测试都通过了。 I'm not sure which object(s) natively define toJSON - and this means it may need to be part of a hardcoded list of method/property names that have to be blacklisted.我不确定哪些对象本身定义了toJSON - 这意味着它可能需要成为必须列入黑名单的方法/属性名称的硬编码列表的一部分。 But I'm not sure how to find out every one of these property methods that needs to be blacklisted.但我不确定如何找出需要列入黑名单的一种属性方法。 So we need to know the best way anyone can generate this list, and keep it updated.所以我们需要知道任何人都可以生成这个列表的最佳方式,并保持更新。

I did find that using Object.freeze(Object.prototype) helps, but I don't think methods like toJSON exist on the prototype.我确实发现使用Object.freeze(Object.prototype)帮助,但我认为原型中不存在像toJSON这样的方法。

How can we make sure the property being set is essentially not already defined on vanilla objects?我们如何确保设置的属性基本上尚未在 vanilla 对象上定义? (ie constructor ) (即constructor

It is more important to prevent a key from being accessed on the wrong object than to validate/protect object keys themselves.防止密钥在错误的对象上被访问比验证/保护对象密钥本身更重要 Designating certain object keys as 'unsafe' and avoiding accessing just these regardless of the circumstances is just another form of the 'sanitizing' anti-pattern .将某些对象键指定为“不安全”并避免在任何情况下只访问这些键只是“消毒”反模式的另一种形式。 If the object doesn't contain sensitive data in the first place, there is no risk of it being exfiltrated or modified by untrusted inputs.如果对象首先不包含敏感数据,则不存在被不可信输入窃取或修改的风险。 You don't need to worry about accessing src or innerHTML if you don't access it on a DOM node;如果不在 DOM 节点上访问,则无需担心访问srcinnerHTML you don't need to worry about exposing eval if you don't perform lookups on the global object.如果不对全局对象执行查找,则无需担心暴露eval As such:像这样:

  • Only use bracket notation with objects that either are arrays or specifically contain nothing other than a mapping from arbitrary strings to other values (those usually constructed by object literal notation);仅对数组或特别包含从任意字符串到其他值的映射(通常由对象文字表示法构造的那些)的对象使用方括号表示法; this kind of object I'm going to call map-like below.这种对象我将在下面称为类似地图的对象。 When using map-like objects, also ensure the following:使用类似地图的对象时,还要确保以下几点:
    • that you never store functions (or, in later versions of ECMAScript, classes or proxies to either) directly in a map-like object.你永远不会将函数(或者,在 ECMAScript 的更高版本中,类或代理)直接存储在类似地图的对象中。 This is to avoid problems when keys like 'toJSON' or 'then' map to functions that the language may then interpret as methods that modify the behaviour of the object.这是为了避免在诸如'toJSON''then''toJSON'键映射到语言可能随后将其解释为修改对象行为的方法的函数时出现问题。 If, for some reason, you need to store functions in a map-like object, put the function in a wrapper like { _: function () { /* ... */ } } .如果出于某种原因,您需要将函数存储在类似地图的对象中,请将函数放入类似{ _: function () { /* ... */ } }的包装器中。
    • that you never coerce map-like objects to strings using built-in language mechanisms: the toString method, the String constructor (with or without new ), the + operator or Array.prototype.join .永远不要使用内置语言机制将类似映射的对象强制转换为字符串: toString方法、 String构造函数(带或不带new )、 +运算符或Array.prototype.join This is to avoid triggering problems when the 'toString' key is set on a map-like object, as even a non-function value will prevent the default coercion behaviour from occurring and will instead throw a TypeError .这是为了避免在类似地图的对象上设置'toString'键时触发问题,因为即使是非函数值也会阻止默认强制行为的发生,而是会抛出TypeError
  • When accessing arrays, make sure the index is indeed an integer.访问数组时,请确保索引确实是整数。 Consider also using built-in methods like push , forEach , map or filter that avoid explicit indexing altogether;还可以考虑使用内置方法,如pushforEachmapfilter ,它们完全避免了显式索引; this will reduce the number of places you will need to audit.这将减少您需要审核的地方数量。
  • If you ever need to associate arbitrary data with an object with a relatively fixed set of keys, eg a DOM node, window or an object you defined with class (all of which I'll call class-like below), and for some reason WeakMap is not available, put the data it on a hardcoded key;如果您需要将任意数据与具有一组相对固定键的对象相关联,例如 DOM 节点、 window或您使用class定义的对象(我将在下面将所有这些称为类类),并且出于某种原因WeakMap不可用,把它的数据放在一个硬编码的键上; if you have more than one such data item, put it in a map-like object a stored on a hardcoded key.如果您有多个这样的数据项,请将其放入存储在硬编码键上的类似地图的对象中。

Even when following the above though, you may still fall victim to an injection or exfiltration attack by inadvertently accessing a property of Object.prototype .即使按照上述操作,您仍然可能因无意中访问Object.prototype的属性而成为注入或渗漏攻击的Object.prototype Particularly worrying are constructor and various built-in methods (which can be leveraged to access the Function object, and ultimately perform arbitrary code execution), and __proto__ (which can be used to modify the prototype chain of an object).特别令人担忧的是constructor和各种内置方法(可用于访问Function对象,并最终执行任意代码执行)和__proto__ (可用于修改对象的原型链)。 To protect against those threats, you may try some of the following strategies.为了防范这些威胁,您可以尝试以下一些策略。 They are not mutually exclusive, but for the sake of consistency it may be preferable to stick with just one.它们并不相互排斥,但为了一致性起见,最好只使用一个。

  • Mangle all keys : this is probably the (conceptually) simplest option, portable even to engines from the days of ECMAScript 3, and robust even against future additions to Object.prototype (as unlikely as they are). Mangle all keys :这可能是(概念上)最简单的选项,甚至可以移植到 ECMAScript 3 时代的引擎,并且即使将来添加到Object.prototype也很Object.prototype (尽管它们不太可能)。 Simply prepend a single non-identifier character to all keys in map-like objects;只需在类地图对象中的所有键前添加一个非标识符字符即可; this will safely namespace away untrusted keys from all reasonably conceivable JavaScript built-ins (which presumably should have names that are valid identifiers).这将安全地从所有可以合理想象的 JavaScript 内置函数(大概应该具有有效标识符的名称)中安全地命名空间远离不受信任的键。 When accessing map-like objects, check for this character and strip it as appropriate.访问类似地图的对象时,请检查此字符并根据需要对其进行剥离。 Following this strategy will even make concerns about methods like toJSON or toString mostly irrelevant.遵循此策略甚至会使对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; } }

    Mangling all keys indiscriminately ensures that you will not end up confusing unmangled keys for mangled keys or vice versa.不加选择地修改所有键可确保您最终不会将未修改的键与损坏的键混淆,反之亦然。 For example if, like in the question, you mangle 'constructor' into 'SAFE_constructor' , but leave 'SAFE_constructor' itself as-is, then after mangling both keys will end up referring to the same data, which may be a security vulnerability in itself.例如,如果像在问题中一样,您将'constructor''SAFE_constructor' ,但将'SAFE_constructor'本身保留'SAFE_constructor' ,那么在修改两个键后最终将引用相同的数据,这可能是本身。

    A drawback of this approach is that the prefix character is going to end up in JSON, if you ever serialise such a map-like object.这种方法的一个缺点是前缀字符将在 JSON 中结束,如果你曾经序列化过这样一个类似地图的对象。

  • Enforce direct property access .强制直接访问属性 Read accesses can be guarded with Object.prototype.hasOwnProperty , which will stop exfiltration vulnerabilities, but will not protect you from inadvertently writing to __proto__ .可以使用Object.prototype.hasOwnProperty保护读取访问,这将阻止Object.prototype.hasOwnProperty漏洞,但不会防止您无意中写入__proto__ If you never mutate such a map-like object, this should not be a problem.如果你从不改变这样一个类似地图的对象,这应该不是问题。 You can even enforce immutability using Object.seal .您甚至可以使用Object.seal强制执行不变性。 If don't want that though, you may perform property writes via Object.defineProperty , available since ECMAScript 5, which can create properties directly on the object, bypassing getters and setters.如果不想这样,您可以通过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; } }
  • Clear the prototype chain : make sure map-like objects have an empty prototype chain.清除原型链:确保类似地图的对象有一个空的原型链。 Create them via Object.create(null) (available since ECMAScript 5) instead of {} .通过Object.create(null) (从 ECMAScript 5 开始可用)而不是{}创建它们。 If you created them via direct object literals before, you can wrap them in Object.assign(Object.create(null), { /* ... */ }) ( Object.assign is available since ECMAScript 6, but easily shimmable to earlier versions).如果您之前通过直接对象字面量创建它们,您可以将它们包装在Object.assign(Object.create(null), { /* ... */ })Object.assign自 ECMAScript 6 起可用,但很容易 shimmable早期版本)。 If you follow this approach, you can use the bracket notation as usual;如果你遵循这种方法,你可以像往常一样使用括号表示法; the only code you need to check is where you construct the map-like object.您需要检查的唯一代码是您构造类似地图的对象的位置。

    Objects created by JSON.parse will by default will still inherit from Object.prototype (although modern engines at least add a JSON key like __proto__ directly on the constructed object itself, bypassing the setter from the prototype's descriptor).默认情况下,由JSON.parse创建的对象仍将继承自Object.prototype (尽管现代引擎至少会直接在构造的对象本身上添加像__proto__这样的 JSON 键,绕过原型描述符中的 setter)。 You can either treat such objects as read-only, and guard read accesses by hasOwnProperty (as above), or strip away their prototypes by writing a reviver function that calls Object.setPrototypeOf .您可以将此类对象视为只读对象,并通过hasOwnProperty (如上所述)保护读取访问,或者通过编写调用Object.setPrototypeOf的 reviver 函数来剥离它们的原型。 A reviver function can also use Object.seal to make an object immutable. 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; }); }
  • Use Map s instead of map-like objects : using Map (available since ECMAScript 6) allows you to use keys other than strings, which isn't possible with plain objects;使用Map代替类似地图的对象:使用Map (自 ECMAScript 6 起可用)允许您使用字符串以外的键,这对于普通对象是不可能的; but even just with string keys you have the benefit that entries of the map are completely isolated from the prototype chain of the map object itself.但即使只是使用字符串键,您也可以享受地图条目与地图对象本身的原型链完全隔离的好处。 Items in Map s are accessed by .get and .set methods instead of the bracket notation and cannot clash with properties at all: the keys exist in a separate namespace. Map的项目通过.get.set方法而不是括号表示法访问,并且根本不会与属性冲突:键存在于单独的命名空间中。

    There is however the problem that a Map cannot be directly serialized into JSON.但是有一个问题,一个Map不能直接序列化成 JSON。 You can remedy this by writing a replacer function for JSON.stringify that converts Map s into plain, prototype-free map-like objects, and a reviver function for JSON.parse that turns plain objects back into Map s.您可以通过为JSON.stringify编写一个JSON.stringify函数来解决这个问题,该函数将Map s 转换为普通的、无原型的类似 map 的对象,并为JSON.parse编写一个 reviver 函数,将普通对象转换回Map s。 Then again, naïvely reviving every JSON object into a Map is also going to cover structures I called 'class-like' above, which you probably don't want.再说一次,将每个JSON 对象天真地恢复为Map也将涵盖我在上面称为“类类”的结构,您可能不想要这种结构。 To distinguish between them, you might need to add some kind of schema parameter to your JSON-parsing function.要区分它们,您可能需要向 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; }); }

If you ask me for my preference: use Map if you don't need to worry about pre-ES6 engines or JSON serialisation;如果您问我的偏好:如果您不需要担心 ES6 之前的引擎或 JSON 序列化,请使用Map otherwise use Object.create(null) ;否则使用Object.create(null) and if you need to work with legacy JS engines where neither is possible, mangle keys (first option) and hope for the best.如果您需要使用两种都不可能的遗留 JS 引擎,请修改键(第一个选项)并希望最好。

Now, can all this discipline be enforced mechanically?现在,所有这些纪律都可以机械地执行吗? Yes, and it's called static typing.是的,它被称为静态类型。 With good enough type definitions, TypeScript should be able to catch cases where class-like objects are accessed in map-like fashion and vice versa.有了足够好的类型定义,TypeScript 应该能够捕获以类映射方式访问类类对象的情况,反之亦然。 It can even catch some cases where an object with an unwanted prototype appears:它甚至可以捕获出现具有不需要的原型的对象的某些情况:

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

Bear in mind this is no panacea, however.但是请记住,这不是灵丹妙药。 The above type definition may catch the most obvious mistakes, but it's not too hard to come up with a case which it will not detect.上面的类型定义可能会发现最明显的错误,但是想出一个它不会检测到的情况并不难。

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

相关问题 javascript 没有括号表示法切片吗? 即字符串[0:4] - Does javascript not have bracket notation slicing? I.e. string[0:4] Javascript方括号表示法多个动态属性 - Javascript Square Bracket Notation Multiple Dynamic Properties JavaScript和方括号表示法 - JavaScript and square bracket notation 如何用方括号表示法实现这个 curry function "add"? - How do I achieve this curry function "add" with square bracket notation? 我可以使用方括号中的表达式通过“this”关键字访问 class 的属性吗? - Can I use expression in square bracket to access property of class by "this" keyword? javascript 中的括号符号如何访问 object 的属性? - How bracket notation in javascript access the property of an object? 如何修复括号vs点表示法Angular.js - How can I fix bracket vs dot notation Angularjs 如何使用括号表示法调用成员函数? - How can I invoke a member function using bracket notation? 如何使用方括号表示法使用多个变量来调用嵌套对象? - How do I use Square Bracket Notation to use multiple variables to call nested objects? 仅使用属性访问器(点符号或方括号符号),如何直接设置未定义的嵌套属性? - Using only property accessors (dot notation or bracket notation), how do I set undefined nested properties directly?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM