Securely set unknown property (mitigate square bracket object injection attacks) utility function

我的梦境 提交于 2020-12-28 07:06:01

问题


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). 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

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. 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 an int, this is obviously always safe.

One situation I don't think I have handled perfectly, are square bracket assignments like this:

someObject[somePropertyPotentiallyDefinedFromBackend] = someStringPotentiallyMaliciouslyDefinedString

The status quo for StackOverflow is to "show me all the code" - when you are going through a codebase and fixing these in hundreds upon hundreds of instances, it takes a lot longer to go and read the code that comes before one of these assignments. Furthermore, we want to make sure this code stays secure as it's modified in the future.

How can we make sure the property being set is essentially not already defined on vanilla objects? (i.e. constructor)

Attempted to solve this myself, but missing some pieces. This will eventually be edited out but is left here for context.

So I think the easiest way to solve this issue is with a simple util, safeKey defined as such:

// 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;
    }
  };
})();

We could also write a util safeSet - instead of this:

obj[key] = value;

You would do this:

safeSet(obj, key, value)

Tests for safeKey (failing):

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

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). 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.

This util 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. 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 methods like toJSON I don't think exist on the prototype.

Here's another little test case:

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

Related: all ways to eval string of javascript: https://www.everythingfrontend.com/posts/studying-javascript-eval.html I sent tweet to author mentioning the constructor loophole.


回答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 are specifically used to contain arbitrary key-value mappings and no other things, especially no methods (the latter kind of object I'm going to call map-like below). For such objects, also ensure the following:
    • that the values you assign in a map-like object are never functions (to avoid problems with a toJSON key or anything similar);
    • that you never directly convert map-like objects to strings using the toString method, the String constructor, type coercion, Array.prototype.join or other built-in mechanism (to avoid problems with a toString key); if you ever need to convert map-like objects to strings, write a function explicitly created for the purpose, without adding it as a method to the object.
  • 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; this will reduce the number of places you will need to audit.
  • If you ever need to break the above rule and use the bracket notation with objects with a relatively fixed set of keys, e.g. DOM nodes, window or an object you defined with class (all of which I'll call class-like below), either make sure the keys come from trusted sources or filter/validate them.
    • In particular, if you want to associate a class-like object with data whose lifetime should be tied to that object, either use WeakMap or (if that isn't available) put it on a key you control. (If you have more than one piece of such data that you want to keep together, you may put it inside a map-like object, itself stored on a key controlled by you.)

Unfortunately, as you noticed, the prototype chain (in particular, Object.prototype) complicates things here somewhat: it defines property descriptors that are by default available on any object. 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).

Below I list some strategies you may consider employing to mitigate the problem of arbitrary string keys clashing with built-ins in Object.prototype. They are not mutually exclusive, but for the sake of consistency it may be preferable to stick with just one.

  • Just transform 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 like toJSON or toString mostly irrelevant. 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 performing property accesses directly on the object. 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 using Object.seal.

    If you insist though, you may perform property writes via Object.defineProperty, available since ECMAScript 5, which can create properties directly on the object, without involving getters and setters.

    In short, you can simply proxy all property accesses through these two functions:

function rawget(obj, key) {
    if (Object.prototype.hasOwnProperty.call(obj, key))
        return obj[key];
}

function rawset(obj, key, val) {
    Object.defineProperty(obj, key, {
        value: val,
        writable: true,
        enumerable: true,
        configurable: true
    });
    return val;
}
  • Strip away Object.prototype: 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 in Object.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.

    Unfortunately though, objects created by JSON.parse 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). 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. A reviver function can also use Object.seal to make an object immutable.

  • Use Maps 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; 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 Maps 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 for JSON.stringify that converts Maps into plain, prototype-free map-like objects, and a reviver function for JSON.parse that turns plain objects back into Maps. Then again, naïvely reviving every JSON object into a Map is also going to cover class-like objects, 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.

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, transform all keys (first option) and hope for the best.

Now, can all this discipline be enforced mechanically? I am not aware of a tool which does that. Presumably such a tool would allow annotating variables as containing map-like objects for which bracket accesses are valid, and probably also allow annotating properties of other objects so that map-like objects not directly contained in variables are also covered. Maybe it should even propagate such annotations through function calls. It should also ensure that such map-like objects are properly constructed, without a prototype. At this point, you're pretty much writing a type checker, so perhaps TypeScript will be worth checking out; then though, I am not aware if TypeScript is capable of this either.



来源:https://stackoverflow.com/questions/57960770/securely-set-unknown-property-mitigate-square-bracket-object-injection-attacks

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!