close search bar

Sorry, not available in this language yet

close language selection

AngularJS 1.6: Life outside the sandbox

David Johansson

Dec 27, 2016 / 8 min read

AngularJS 1.6 was recently released. With this release comes several impactful changes. One such change to note is the removal of the expression sandbox. This was a predicted change that was first announced in early September. If you haven’t already evaluated the impact of this on your Angular code in preparation for the changes, it’s high time to do so.

Is your code mature enough to survive life outside the sandbox?

The answer is so simple that it might surprise you–life outside the sandbox is no different than life inside. It should have no impact on the security of your AngularJS application. You see, Angular expressions weren’t sandboxed for security reasons in the first place. It was not intended to act as a security boundary. Therefore, the various ‘sandbox escapes‘ published by security researchers were never considered to be vulnerabilities. Even so, the Angular team continued patching the sandbox until the recent release of 1.6.

A number of developers were assuming (incorrectly) that the expression sandbox would protect them against code injection. This leads to a false sense of security, proving time and time again to be ineffective. Angular is doing the right thing to remove the sandbox altogether. If a new expression sandbox escape payload affects your security posture, it’s not due to a security problem with sandbox implementation. Rather, it’s because of your broken code. That’s where the root cause of the issue lies. That’s also where there is the need for a solution.

The problem was never that expression code could break out of the sandbox. Rather, it was that your code allows untrusted data to execute as an Angular expression. The sandbox didn’t make much of a difference. At best, it could somewhat reduce the impact of such expression injection. Even with a ‘perfect’ sandbox (it never was), untrusted data executing as an expression is still code execution within your Angular application. It could access data and alter logic, even though you couldn’t directly pop an alert box as in a classic cross-site scripting (XSS) proof of concept.

Sorting out expression injections in your orderBy filters.

Now that we’ve covered why sandbox removal doesn’t make any real difference to the security model of an Angular application, make sure that you fix the root cause of expression injections in your code. Angular’s security guide lists most of these issues. Many developers already know to avoid mixing client- and server-side templates. Many also know that ‘$eval is evil’ when used with untrusted content. However, one instance where expression injection may not be an obvious issue is when you’re using the orderBy filter to sort collections.

Let’s look at the example provided in the Angular documentation for the orderBy filter. We’ve defined a collection of friends, each described by name, phone, and age fields.

$scope.friends = [

    {name: ‘John’,   phone: ‘555-1212’,  age: 10},

    {name: ‘Mary’,   phone: ‘555-9876’,  age: 19},

    {name: ‘Mike’,   phone: ‘555-4321’,  age: 21},

    {name: ‘Adam’,   phone: ‘555-5678’,  age: 35},

    {name: ‘Julie’,  phone: ‘555-8765’,  age: 29}

  ];

 

To display these, a simple table is added to the Angular template:

<table class=”friends”>

    <tr>

      <th>Name</th>

      <th>Phone Number</th>

      <th>Age</th>

    </tr>

    <tr ng-repeat=”friend in friends | orderBy:’-age'”>

      <td>{{friend.name}}</td>

      <td>{{friend.phone}}</td>

      <td>{{friend.age}}</td>

    </tr>

  </table>

 

Note the use of the orderBy filter in the ngRepeat directive. It’s passed the string ‘-age’ to specify that the list should be ordered by the field ‘age’ in reverse order (hence the minus sign). The next example in Angular’s documentation shows how the code is modified to change the sort parameter dynamically. This allows you to click on a table header and sort by that field. To accomplish this, define the scope property ‘propertyName’ and use that in the orderBy filter instead of a static string. As you click on the table header, ‘propertyName’ is set to the corresponding value. Thus, the table sorts accordingly.

angularjs1

Figure 1: Table sorted through orderBy filter.

Let’s say that you want to create an external link to this page. The table sorts according to a field name specified in the link. For example, say that the goal is for https://www.example.org/angular/orderby.html#field=name to load and automatically sort the friends table by the ‘name’ field. This is easily accomplished by assigning the scope property ‘propertyName’ on initial load to the value of the URL ‘field’ parameter. Changing the link to https://www.example.org/angular/orderby.html#field=age now automatically sorts the table based on the age when loaded.

This is where the trouble begins. Initiating the value of the scope property ‘propertyName’ from a URL parameter provides an attacker the ability to create a link to the application with an arbitrary value for the field parameter. This may not seem like much of an issue. After all, what could an attacker do by defining the sort order for your table? It turns out to be a lot more than you would think.

An attacker can use it to inject arbitrary expressions into your Angular application and execute arbitrary code in its context. How could a simple thing like sorting a table in Angular based on a user-supplied field name lead to XSS?

js alert box

Figure 2: JavaScript alert box injected through orderBy filter.

To understand this issue, let’s look deeper into how the orderBy filter works in Angular. Sorting a collection of items takes place using a version of the Schwartzian Transform idiom from Perl. This consists of three basic steps:

  1. Feed the unsorted list through a mapping function. Thus, creating a new collection with the original item and the calculated value to use for the comparison.
  2. Sort the new list in order based on the value calculated for each item in step 1.
  3. Create the sorted list by recreating the original list based on the sorting order determined in step 2.

The expression injection vulnerability arises in the first step of this algorithm. When supplying a string as the predicate for sorting elements in a collection according to a specific property, it’s actually evaluating that predicate as an expression against each element in the collection. The result of that is used for sorting the elements. The filter orderBy:’age’ evaluates ‘age’ as an expression against each element in the collection we provided to the filter. For example, for the first element {name: ‘John’,   phone: ‘555-1212’,  age: 10} the result is 10. To do this, the orderBy filter injects a dependency to the $parse service and calls $parse on the provided predicate to convert the expression into a function:

if (predicate !== ”) {

          get = $parse(predicate);

This function is then called on each item in the collection to yield the comparison object:

          return getPredicateValue(predicate.get(value), index);

When the get-function is invoked on each item in the collection, the code that was created when passing the predicate to the $parse() service is executed. This is why Angular’s security guidelines warn against passing any expression generated based on untrusted data as a predicate to the orderBy filter. An attacker-controlled value leads to expression injection. This can usually be exploited for arbitrary script execution (e.g., XSS).

Important note: Angular security documentation only mentions the risk associated with user-provided content in the orderBy pipe (e.g., use with directives in HTML template bindings). The same issue applies when using the orderBy filter directly in JavaScript (e.g., in a controller). The following code sample may introduce the same vulnerability if the highlighted expression parameter contains untrusted data:

$filter(‘orderBy’)(collection, expression, reverse, comparator)

The orderBy filter can also be injected as a dependency in to the controller. It can thus be used directly, without the $filter service:

angular.module(‘orderByExample’, [])

.controller(‘ExampleController’, [‘$scope’, ‘orderByFilter’, function($scope, orderByFilter) {

$scope.friends = orderByFilter(collection, expression, reverse, comparator);

}

Where the sandbox may have saved you.

Earlier we noted that expressions in Angular weren’t sandboxed for security reasons. Additionally, in practice, it didn’t provide any protection against XSS. For each version of Angular containing the sandbox (up to 1.5.9), there have been working exploit payloads published that escape the sandbox to inject arbitrary JavaScript.

There is one exception. Most of these payloads don’t work for expressions injected into the orderBy filter. The reason for this is that all escape payloads published for the recent versions of Angular (e.g., 1.3.1 and later) rely on calls to $eval–a method in the Angular scope. Normally, expressions are evaluated in the context of a scope object, and hence, have access to call this method. However, the expressions injected into the orderBy filter are evaluated against each item in a collection instead of a scope object. The execution context is much more limited. Scope methods like $eval aren’t directly available. Thus, well-known exploit payloads fail to break out of the sandbox.

With further research, it’s likely that people will find other ways to escape the sandbox, even in this more restricted execution context. Until then, it’s plausible that the expression sandbox could save you from arbitrary script execution in some instances.

Although the removal of the sandbox in Angular 1.6 shouldn’t have any impact on the security of your Angular application, there are cases of lesser known expression injection vulnerabilities in which it actually may, just may, have saved you in the past.

It’s time to let go of the sandbox.

It’s tempting to stick with 1.5.9 to retain the sandbox. After all, it makes you feel a little more secure to know it’s there, doesn’t it? As tempting as it may be, don’t!

It gives you a false sense of security. More importantly, it doesn’t address the real problem. Make sure that untrusted data is never used as code in an expression without strict input validation, such as an exact match or numeric validation. For example, the injections in the orderBy filter that we have already discussed could be prevented by validating that the input matches one of the fields in the collection objects (e.g., ‘name,’ ‘phone,’ or ‘age’). Once achieving this, welcome to the freedom of Angular 1.6. At this point, your code has grown up and you can now trust it to survive life outside the sandbox.

Want to try it out?

If you want to try injecting JavaScript through the orderBy filter yourself, you can test a simple proof of concept vulnerable Angular application here: https://jsfiddle.net/m7gap35d/. For convenience, this application takes user-input for the predicate from a simple textbox instead of a URL parameter. However, the principle is the same. As such, assume it’s an attacker-controlled value.

When you provide input such as “name” as a predicate, you’ll see that the list sorts on that element field. However, when you enter a ‘malicious’ payload such as “constructor.constructor(‘alert(1)’)()” you’ll see that the alert box appears. Injected expressions will not have direct access to the window object. However, from Angular 1.6 there’s no sandbox that actively tries to prevent expressions from calling constructors etc. to invoke arbitrary JavaScript.

Controlling the predicate used in the orderBy filter allows you to execute arbitrary JavaScript code in the context of the Angular application. In other words, you have a working XSS exploit. The alert box appears multiple times due to the injected expression executing on each element in the collection.

To fix this expression injection issue in our orderBy example, we can perform an exact-match input validation on the userData variable to ensure that it only references a valid field on the friends object (e.g., ‘name,’ ‘phone,’ or ‘age’). A fixed example with exact-match validation of the untrusted input can be found here: https://jsfiddle.net/43d42r59/. Note that it doesn’t accept arbitrary input to be assigned to the predicate, only the specified field names are accepted. The proof of concept ‘XSS’ payload will no longer execute in this example as it will not pass the validation control we have added.

 

 

Written in coordination with Lewis Ardern.

Continue Reading

Explore Topics