[英]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 节点上访问,则无需担心访问
src
或innerHTML
; you don't need to worry about exposing eval
if you don't perform lookups on the global object.如果不对全局对象执行查找,则无需担心暴露
eval
。 As such:像这样:
'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 () { /* ... */ } }
的包装器中。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
。push
, forEach
, map
or filter
that avoid explicit indexing altogether;push
、 forEach
、 map
或filter
,它们完全避免了显式索引; this will reduce the number of places you will need to audit.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;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.遵循此策略甚至会使对
toJSON
或toString
类的方法的担忧几乎无关紧要。
// 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.