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 a number, this is obviously always safe.
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:
// 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). 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 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. 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.
How can we make sure the property being set is essentially not already defined on vanilla objects? (i.e. constructor
)
__proto__
- I'd say typically that would be the attack vector... but this question is going above and beyond to say "how can we make sure all square bracket assignments are safe without needing to understand the full context of any given line of code?" – Lampkinrequire
is not a global and I cannot get this exploit working due to that. Can you possibly add some code showing how the exploit can still be taken advantage of, if it can? – DrusesafeKey('constructor') === safeKey('SAFE_constructor')
, which may lead to a vulnerability in itself! Moreover, if you ever exchange data between systems with different JS engines (and different contents ofObject.prototype
), they are going to disagree about which keys are supposed to be transformed, creating even more problems. – SiansafeKey
can generate a key that triggers a getter; what I pointed out is that it's not an injection, which may lead to conflating things that should be distinguished. For example, if you have a map of user names to some data and you filter them though your function, then an attacker may access data belonging to userconstructor
by naming themselvesSAFE_constructor
. – Sian