TL;DR We can exploit the partially applied functions to "free" the bound arguments and run the originalfunction with new set of unexpected runtime arguments. This is a potentially serious security hole, as it canaffect various browsers and the popular server middleware projects like ExpressJs. It effectivelyremoves the assumption that functions inside closures are private to the closure and cannot beaccessed from the outside.
Review
The exploit uses two basic JavaScript concepts: closures and partial application.
Closures
You can review closures by readingthis blog post. In essence, JavaScriptdoes not have the keywordprivate. Instead it restricts variable's visibility to the functionthat declares it. This is commonly used to create variables that cannot be accessed from the outside
1 | var four = (functionclosure() { |
Partial application review
You can review JavaScript concept of context binding and partial application by readingthis blog post.
JavaScript ES5 includes a native method for binding the values to the function's arguments, and producing a new function. For example
1 | functionadd(a, b) {return a + b; } |
We assume that given only a partially applied function reference, likeadd2
oradd2to5
,there is no way to get to the original function referenceadd
. This can be used to conveniently hidethe original unprotected code inside a closure, exporting only a partially applied function that issafe for the outside users to call.
Here is an example of a closure with an unprotected function and exported partially bound reference.
1 | // take a simple function add, |
Becauseadd
is inside the anonymous closure, we assume there is no way to callit from the outside, except via the returned partially applied reference.This is important in this case because as an exampleadd
has an undesiredbehavior: it crashes if the first argument has value 42. The programmer assumesthatadd
will always run with first argument equal to 2. Yet, this assumption is invalid as I will show next.
Polyfills
The nativeFunction.prototype.bind
method used above is part of the modern ES5 standard.There are a lot of polyfills that provide this method for legacy reasons. One can easily writeabind
polyfill, here is one suggestion that does not implement the context binding, onlythe partial argument application.
1 | // simple bind polyfill |
The main principle is to combine the values given in the original call (theprev
array) with the values given at the later runtime call (thecurr
array). One can use the custombind
similarly to the native one
1 | var add2 =bind(add,2); |
A typical polyfill used in many projects ises5-shim downloaded more than100k times every month. Thees5-shim
library has its ownbind function.Its argument combination code is very similar to the simple "bind" code example from above:
1 | // other stuff |
Attacking polyfills
The goal of the attack is to run theoriginal function instance, replacing thepreviously bound arguments withthe new runtime values. Because the original function might rely on its privacy, it might NOT validate the inputs, assuming that some of them will always be bound to the "safe" values.
Here is how one can achieve this.Let us take a look at the simplebind
polyfill again. Suppouse we inspect thesource of thebound functionadd2
. We will see just the code returned by the polyfill.
1 | var add2 =bind(add,2); |
Typically, I would attack a method like this byfaking its lexical scope,but in this scope there are two variables accessed via the lexical scope:prev
andfn
. Since I want to use the originalfn
function while replacing theprev
, this technique does not work.
Yet, there is a part of the code that I can modify easily: theArray.prototype.concat
call that combinesthe previouslybound valuesprev
with the runtime valuescurr
. I (the attacker) can simplyoverwrite theArray.prototype.concat
call todiscard theprev
array entirely! Here is one attack methodthat works against both the simplebind
andes5-shim
functions
1 | // unapply-attack |
An attacker uses the aboveunapplyAttack
function like this
1 | // attack example |
Again, just to stress the important point: the attack does not rely on calling thetoString()
- only on inspecting thebind
implementation and changing theArray.prototype
method(s) to changethe behavior when combining previously bound values with the new ones.
Another attack example
Suppose we send the user's data to the server
1 | var sendUserData = (function () { |
One can attack even the modern ES5 engines in 3 steps
- delete the native
Function.prototype.bind
- load the
es5-shim
(which many projects do for legacy reasons) - execute the
unapplyAttack
1 | // replace v8's native bind with es5-shim version |
Attacking Express.js
The middleware server stacks in theExpress.js can be attackedif the programmers used partial application toremove the routing boilerplate.Typically, there is a router that executes the middleware functions in order, unless a functionreturned false. For example, let us have a "server" that only allows the restricted functionsto run if the user is logged in and authorized. You can see the "server" code inserver.js
The partial application used in this example is a simple placeholder binderspots
- similar to now common functions in the popular librarieslikelodash#partial andRamda#partial.
1 | // exporting partially applied authorization routing function |
Unless the functionisLoggedIn
andisAuthorized
return true, therestricted
function should never run.But this is only true if the attacker can NOT replace the bound values via theroute
reference. This is simple for thespots library, we can inspect therelevant source code to see how it places the runtime arguments into the placeholder items
1 | // part of spots.js that combines previous "bound" and current values |
Here is the attack
1 | functionunapplyAttack() { |
I am not sure if such placeholder trick is possible for thelodash library. The value combinatorin that library uses plain indices to combine the arrays, without calling the prototype methods, see thewrapper code.
Preventing the unapply attacks
There are steps that both library authors and users can take to stop unapply-style attacks.
From the user's (website author) perspective I only see one solution to these attacks: freezing the built-in prototypes likeArray.prototype
,Function.prototype
,Object.prototype
beforeloading any other code (like trusted libraries or 3rd party code). For example:
1 | // prevent the second attack |
As far as I see, unless one can show a very convincing use case, modifying the built-in prototypes shouldnot be possible. I suggest freezing the prototypes after loading the system / shim JavaScript libraries butbefore loading any of the application-specific or 3rd party plugin code.
The library authors can take steps to lessen the chance of the attack in a couple of ways.One, keep a reference to the original (hopefully uncompromised) methods used to combine arguments.
1 | // inside the shim library |
Second, instead of using methods to combine previously bound / runtime arguments, use plain for loops, which wouldbe harder to change externally
1 | // inside the shim library |
Conclusion
JavaScript is a wonderful and flexible language, but some malicious code can use the language's prototype methods to break the expectation of "privacy" inside the closures. It will take more thanjust relying on the originalFunction.prototype.bind
in all cases; modern applications use otherstyles of partial application using client-space implementations:partial application from the right,position index application,application with placeholders andapplication by name. It is up to the library's authors to provide reasonably safe implementations, and up to the user to be vigilant against loading malicious code.
Finally, I guess this attack can be extended to rebinding the original function to a different context.
Update 1
After I contacted the es5-shim maintainers, thebind
polyfill has been made to be more robust to messing with theArray.prototype.concat
method, see thechange.
Update 2
I wrote small library to freeze the common prototypes before loading untrusted 3rd party code.Hope that this is enough to prevent these attacks.
1 | <scriptsrc="//cdn/jquery.js"></script> |
Update 3
A lot of feedback I have received for this blog post focuses in my opinion on thewrong aspect. Yes, the attacker would need to run malicious code. Yes, JavaScript is a dynamiclanguage. But the problem is that by using 3rd party code, unreviewed, we trust that codetoo much. In essence we are saying that if we keep the door locked, we don't need a safeinside the house. The privacy mechanism the closures give us is like a safe. By allowing3rd party code to modify prototypes we are removing the safe's back wall!