How Can I Mitigate Injection/exfiltration Attacks From Dynamic Property Accesses (i.e. Square Bracket Notation) In Javascript?
Solution 1:
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; you don’t need to worry about exposing eval
if you don’t perform lookups on the global object. 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. 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. If, for some reason, you need to store functions in a map-like object, put the function in a wrapper like{ _: function () { /* ... */ } }
. - that you never coerce map-like objects to strings using built-in language mechanisms: the
toString
method, theString
constructor (with or withoutnew
), the+
operator orArray.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 aTypeError
.
- that you never store functions (or, in later versions of ECMAScript, classes or proxies to either) directly in a map-like object. This is to avoid problems when keys like
- When accessing arrays, make sure the index is indeed an integer. Consider also using built-in methods like
push
,forEach
,map
orfilter
that avoid explicit indexing altogether; 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, e.g. a DOM node,
window
or an object you defined withclass
(all of which I’ll call class-like below), and for some reasonWeakMap
is not available, put the data it on a hardcoded key; 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
. 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). 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). 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). When accessing map-like objects, check for this character and strip it as appropriate. Following this strategy will even make concerns about methods liketoJSON
ortoString
mostly irrelevant.// replacement for (key in maplike)functionmaplikeContains(maplike, key) { return ('.' + key) in maplike; } // replacement for (maplike[key])functionmaplikeGet(maplike, key) { return maplike['.' + key]; } // replacement for (maplike[key] = value)functionmaplikeSet(maplike, key, value) { return maplike['.' + key] = value; } // replacement for the for-in loopfunctionmaplikeEach(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.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.
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__
. If you never mutate such a map-like object, this should not be a problem. You can even enforce immutability usingObject.seal
. If don’t want that though, you may perform property writes viaObject.defineProperty
, available since ECMAScript 5, which can create properties directly on the object, bypassing getters and setters.// replacement for (key in maplike)functionmaplikeContains(maplike, key) { returnObject.prototype.hasOwnProperty.call(maplike, key); } // replacement for (maplike[key])functionmaplikeGet(maplike, key) { if (Object.prototype.hasOwnProperty.call(maplike, key)) return maplike[key]; } // replacement for (maplike[key] = value)functionmaplikeSet(maplike, key, value) { Object.defineProperty(maplike, key, { value: value, writable: true, enumerable: true, configurable: true }); return value; } // replacement for the for-in loopfunctionmaplikeEach(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{}
. If you created them via direct object literals before, you can wrap them inObject.assign(Object.create(null), { /* ... */ })
(Object.assign
is available since ECMAScript 6, but easily shimmable to earlier versions). 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 fromObject.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). You can either treat such objects as read-only, and guard read accesses byhasOwnProperty
(as above), or strip away their prototypes by writing a reviver function that callsObject.setPrototypeOf
. A reviver function can also useObject.seal
to make an object immutable.functionmaplikeNew(maplike) { returnObject.assign(Object.create(null), maplike); } functionjsonParse(json) { returnJSON.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: usingMap
(available since ECMAScript 6) allows you to use keys other than strings, which isn’t possible with plain objects; 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 inMap
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.There is however the problem that a
Map
cannot be directly serialized into JSON. You can remedy this by writing a replacer function forJSON.stringify
that convertsMap
s into plain, prototype-free map-like objects, and a reviver function forJSON.parse
that turns plain objects back intoMap
s. Then again, naïvely reviving every JSON object into aMap
is also going to cover structures I called ‘class-like’ above, which you probably don’t want. To distinguish between them, you might need to add some kind of schema parameter to your JSON-parsing function.functionjsonParse(json) { returnJSON.parse(json, function (key, value) { if (typeof value === 'object' && value !== null && !Array.isArray(value)) returnnewMap(Object.entries(value)); return value; }); } functionjsonStringify(value) { returnJSON.stringify(value, function (key, value) { if (value instanceofMap) returnObject.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; otherwise use 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.
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. It can even catch some cases where an object with an unwanted prototype appears:
typeMaplike<T> = {
[K: string]: T|undefined;
constructor?: T;
propertyIsEnumerable?: T;
hasOwnProperty?: T;
toString?: T;
toLocaleString?: T;
valueOf?: T;
};
constplain: { [K: string]: string } = {};
constnaked: Maplike<string> = Object.create(null); // OKconsterror: Maplike<string> = {}; // type errorfunctionf(k: string) {
naked[k] = 'yay'; // OK
plain[k] = 'yay'; // OKdocument.body[k] = 'yay'; // type error
}
console.info(plain.toString()); // OKconsole.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.
Post a Comment for "How Can I Mitigate Injection/exfiltration Attacks From Dynamic Property Accesses (i.e. Square Bracket Notation) In Javascript?"