This is a response to a post by Matteo Collina
Define We
Matteo's post argues that eliminating prototype pollution is not a reachable goal for Node.js and also not a security but an integrity issue really.
I agree with both of these points. Our titles use the same word We to refer to different groups of interest in my deliberate choice to sound more confrontational 😅
Matteo said the problem is:
asking core to compensate for a bug that lives upstream in an application
Node.js should not be expected to plug all possible prototype pollution in built-in modules. That being said...
What I'd like to explore is that we - the authors of apps can use tools that we - the people behind HardenedeJS and LavaMoat make, to harden specific codebases running in Node.js, against prototype pollution.
There are two ways to mitigate prototype pollution.
One is called Defensive Coding, and I had a lot of fun teaching people how to pivot around a JavaScript application with increasingly horrible prototype pollution and how to write code that avoids losing integrity in the face of these attacks during workshops at DEFCON, NodeConfEU and more always reaching the conclusion that Defensive Coding is hard to scale to a larger codebase, which leads to...
Another approach that relies on hardening the environment upfront to mitigate entire classes of attacks on integrity.
HardenedJS
The premise of HardenedeJS is explained on the website much better than I ever could (do watch the introduction video), so let's focus on the basic step of hardening your environment to eliminate the possibility of prototype pollution on intrinsics.
The core idea is to do something like:
Object.freeze(Object.prototype)
and get away with the consequences of doing so.
That way, when someone attempts the most powerful prototype pollution attack, by finding a way to put a field on Object.prototype, they'll get an error instead
TypeError: Cannot add property admin, object is not extensible
The tool to harden the environment is available in the package called SES and is a part of what LavaMoat uses to make the security policy possible (but that's a different story)
DIY
So how do I harden?
You can always go with
node --frozen-intrinsics
but that method offers no means of dealing with certain consequences.
SES comes with some opinionated fixes that both make for a better foundation for secure computation and mitigate some of the issues you might get from freezing prototypes.
import('ses')
lockdown()
Is it that simple? Well, you probably want to start with a less strict version of lockdown to maximize ecosystem compatibility and have the ability to sprinkle in polyfills.
This example includes the options we use to harden the environment under React Native in the @lavamoat/react-native-lockdown package
import('ses')
repairIntrinsics({
errorTaming: 'unsafe',
consoleTaming: 'unsafe',
errorTrapping: 'none',
unhandledRejectionTrapping: 'none',
overrideTaming: 'severe',
stackFiltering: 'verbose',
evalTaming: 'unsafe-eval',
})
/* some polyfills could go here */
hardenIntrinsics()
Why am I giving a React Native example in an article about Node.js? Because for Node.js we use a more strict setup under LavaMoat and it goes much further than preventing prototype pollution. Again, a different story, but feel free to check what lavamoat does.
Note:
evalTaming: 'unsafe-eval'is there to disable taking over allevalandnew Functionand introduceCompartment. If you only want the frozen intrinsics, they can be turned off. If you run LavaMoat in your project, they will be on and used to sandbox your dependencies.
Wait, what consequences?
Yes, I mentioned avoiding consequences of freezing intrinsics.
The consequences are why mainstream JS runtimes don't freeze them by default.
1. Polyfills
The difference between prototype pollution and a polyfill is intentions. If we block poisoning the prototypes, we also block improving the prototypes. It presents a challenge to anyone who wants to "simply freeze" intrinsics and proceed to use the JS ecosystem
That's why we split lockdown into 2 stages to squeeze in some polyfills, but you need to make sure they run between repairIntrinsics and hardenIntrinsics calls. Otherwize repairIntrinsics will remove all unexpected fields your polyfills have added to the shared prototypes.
2. Override Mistake
The "override mistake" in JavaScript happens when you try to assign a value to a property on an object whose parent or prototype has a non-writable (frozen or read-only) property with the same name, which throws a TypeError in strict mode and fails silently in sloppy mode.
Freezing intrinsics will cause some of the packages you use to fail because they use own property names that match names existing on their prototype. These packages will need patching, locally or upstream, similar to this patch in axios - assignment needs to be replaced with defineProperty which does not trigger the override mistake.
Luckily, most common cases of override mistake can be mitigated with some extra work that ses does if the overrideTaming: 'severe' option is set.
3. Optimizer gets confused
V8 does a lot of tremendously complex work to optimize JavaScript and some of the optimizations rely on assumptions that may not always be true. So it has to bail out of optimizing functions when there's a risk of compiling them into something that's not correct. One of the reasons some of the routines bail out is that prototypes have been tampered with. In some unfortunate cases the detection catches a frozen prototype being different than the original state it expected.
Here's one example of V8 fixing an issue where unnecessary caution was causing a function iterating over a typed array to not be optimized. It was the most severe case of performance degradation caused by frozen intrinsics.
Why it matters to me
Just like Matteo, I have spent a decade on a mission. I've been trying to mitigate the supply-chain risks and today we're suddenly in a much better position - with package managers agreeing to introduce breaking changes to protect the ecosystem, configurations being available to harden project install time (use @lavamoat/harden to get your project configured) and finally the runtime protections that we're very close to shipping with ESM and TS support in @lavamoat/node.
It's never trivial, but with the right tools and a bit of attention, you can actually prevent entire classes of security/integrity issues in your applications and someone already did most of the work for you .
If you're rolling out LavaMoat tools to a security-critical piece of your application and want to get some guidance, feel free to reach out!
Top comments (0)