When you're starting out trying to land your first pull-request into Node JS, there is a high chance you will come across code like this
For some context, this is the setHeader
method present on all http response objects in Node JS.
If we closely look at the 2 highlighted functions, ObjectCreate
and StringPrototypeToLowerCase
these are the exact same functions as Object.create
and "Hello".toLowerCase()
So the first question that comes to mind is, why use these functions instead of the already existing globals cause we're pretty much duplicating functionality right? Well not really.
These functions are called Primordials
, by definition the closest useful meaning of the word primordial is
Something that existing from the beginning of time.
In Node JS, Primordials are a form of protection against monkeypatching and prototype pollution attacks.
Prototype Pollution
Before we see what Prototype pollution is and what it looks like in action, I expect all readers to be atleast familiar with how object prototypes and inheritance works in Javascript, if you don't know then I suggest reading this article and then come back to this post.
Back to the topic, Let's say our internal code makes use of this function
All it does is to token a authorization header from the request headers and calls the .replace()
method that comes from the String.prototype
object.
All strings in Javascript inherit from the String.prototype
object hence all strings have these common set of methods and properties.
And here is where the fun beings,
if we modify a prototype shared by two or more objects, all objects will reflect this modification! They don’t even have to be in the same scope or otherwise related.
A simple exploit
For a moment, let's say we installed a malicious package which does something useful for us but deep down somewhere in the library we have this code
So what does this code do? Anytime you call the .replace()
method on any string, the hacker gets all the data
like value of the string, function arguments and most importantly YOUR ENVIRONMENT VARIABLES which might contain things like api keys, secrets etc.
And you won't even get to know when the exploit happens because we store a reference to the original
String.prototype.replace
method's implementation in a variable called orig
which can be later used.
So to the user, it works exactly as expected but we've monkeypatched it.
Back to Primordials
Now if these prototype pollution attacks work on user land code eg. Apps, packages etc, then would
they also work if we try to monkeypatch some of the node's internal modules? things like crypto.pbkdf2
which is a commonly used
hashing function.
Well, not really because when a Node process starts up, all the primordials are captured before any user code is permitted to run and Node's internal (core) modules use those so that if user code does try monkeypatching those core bits we'll still be using the safe originals.
Hence Primordials only protect Node's core modules and internals, it's still possible to use this attack against your application code.
Closing up
The purpose of this article was to demystify a tiny part of Node core's source code and also to help others understand why we should avoid installing unknown npm packages that could potentially expose our applications to more attack vectors.
That being said, there are several ways to protect your apps from this type of attack things like using Object.freeze
inside your usercode to
ensure no one extends/mutates any objects in your user land code.
Most tools like npm, github and snyk do scans to look for such vulnerabilities and reports them to developers.