Client-side template injection (CSTI) vulnerabilities arise when client-side template frameworks dynamically embed user input into web pages. During rendering, these frameworks evaluate template expressions, which can be exploited by attackers to inject malicious code, potentially leading to arbitrary code execution and cross-site scripting (XSS) attacks.
Although client-side template injection is a generic issue, we’ll focus on examples from the AngularJS framework as this is the most common.
AngularJS
AngularJS is a JavaScript framework used to build dynamic web applications. Key core concepts for beginners include:
Two-way Data Binding: Automatically synchronizes data between the model (data) and view (UI), so changes in one are reflected in the other.
Directives: Special markers (attributes, elements, or classes) that extend HTML’s capabilities, allowing developers to create custom behavior in the DOM.
Controllers: JavaScript functions that manage the logic behind the view. They control the data and behavior of the application.
Services: Reusable components for handling business logic, such as fetching data or processing input. Services can be injected into controllers or other services.
Modules: Containers for organizing an AngularJS application, grouping related components like controllers, services, and directives.
Dependency Injection: A design pattern that allows AngularJS to manage and inject dependencies (like services) into components automatically.
Filters: Used to format data before displaying it in the view, such as formatting dates or numbers.
Templates: HTML views with embedded AngularJS expressions and directives, defining the structure of the user interface.
Expressions
In Angular, an expression is a snippet of code written inside double curly braces ({{ }}) in the HTML template. AngularJS expressions are much like JavaScript expressions: they can contain literals, operators, and variables.
Without the ng-app directive, AngularJS expressions are treated as plain text and won’t be evaluated. When ng-app is present, AngularJS initializes the application and processes expressions. Angular replaces the expression with the evaluated result in the DOM.
Note
The ng-app directive must be present in the DOM hierarchy above the expression. Typically, Angular applications place ng-app in the root <html> or <body> tag.
The final output displayed on the page is My first expression: 2.
The following example demonstrates how importing the AngularJS framework can introduce vulnerabilities. If user input is embedded without proper sanitization, anyone injecting double curly braces ({{ }}) can execute Angular expressions:
While Angular expressions alone have limited impact, combining them with a sandbox escape can enable arbitrary JavaScript execution, leading to significant damage.
Sandbox
The AngularJS sandbox is a security mechanism designed to restrict access to potentially dangerous objects, like window or document, and sensitive properties, such as __proto__, within AngularJS template expressions.
The sandbox works by parsing an expression, rewriting the JavaScript code, and then using various functions to check for dangerous objects or operations.
With the above example which uses {{ 1 + 1 }} expression, if we set a breakpoint at line 13275 of the angular.js file, we can see the rewritten code of this expression:
When attempt to get the Function constructor with {{constructor.constructor('alert(1)')()}} expression, the console throws an error like this:
<a class='gotoLine' href='#"Error: [$parse:isecfn'>"Error: [$parse:isecfn</a> Referencing Function in Angular expressions is disallowed! Expression: constructor.constructor('alert(1)')()http://errors.angularjs.org/1.4.6/$parse/isecfn?p0=constructor.constructor('alert(1)')() at https://ajax.googleapis.com/ajax/libs/angularjs/1.4.6/angular.js:68:12 at ensureSafeObject (https://ajax.googleapis.com/ajax/libs/angularjs/1.4.6/angular.js:12524:13) at fn (eval at compile (https://ajax.googleapis.com/ajax/libs/angularjs/1.4.6/angular.js:13275:15), <anonymous>:4:172) at Object.expressionInputWatch <a class='gotoLine' href='#as get'>as get</a> (https://ajax.googleapis.com/ajax/libs/angularjs/1.4.6/angular.js:14235:31) at Scope.$digest (https://ajax.googleapis.com/ajax/libs/angularjs/1.4.6/angular.js:15751:40) at Scope.$apply (https://ajax.googleapis.com/ajax/libs/angularjs/1.4.6/angular.js:16030:24) at bootstrapApply (https://ajax.googleapis.com/ajax/libs/angularjs/1.4.6/angular.js:1660:15) at Object.invoke (https://ajax.googleapis.com/ajax/libs/angularjs/1.4.6/angular.js:4476:17) at doBootstrap (https://ajax.googleapis.com/ajax/libs/angularjs/1.4.6/angular.js:1658:14) at bootstrap (https://ajax.googleapis.com/ajax/libs/angularjs/1.4.6/angular.js:1678:12)"]
Explanation of the expression
The constructor.constructor will return the Function constructor:
The ensureSafeObject() function checks if the object is the Function constructor, the window object, a DOM element or the Object constructor. If any of the checks are true it will raise an exception and stop executing the expression. It also prevents access to global variables by making all references to globals look at a object property instead.
ensureSafeMemberName(): ensures that JavaScript properties being accessed are safe by blocking dangerous ones like __proto__.
ensureSafeFunction(): prevents unsafe function calls, such as invoking the Function constructor or using methods like call(), apply(), or bind().
Escaping the Sandbox
A sandbox escape occurs when an attacker manipulates a sandbox environment into treating a malicious expression as harmless. A common method exploits a modified global charAt() function, as demonstrated here:
'a'.constructor.prototype.charAt = [].join
In this attack, the charAt() function, which typically returns a single character from a string, is overridden with [].join. This causes charAt() to return the entire string rather than just one character.
For example, consider the following code snippet. If this.index is 9, the variable ch will contain the string 'x9=9a9l9e9r9t9(919)' instead of a single character:
while (this.index < this.text.length) { var ch = this.text.charAt(this.index); if (ch === '"' || ch === "'") { this.readString(ch); } else if (this.isNumber(ch) || (ch === '.' && this.isNumber(this.peek()))) { this.readNumber(); } else if (this.isIdent(ch)) { this.readIdent(); } // ...}
In this code, charAt() is intended to extract individual characters from this.text for type checking. However, due to the override, charAt() returns a long string instead of a single character.
This modification can lead to Angular misinterpreting expressions like alert(1) as identifiers, not function calls, due to the isIdent() check:
The isIdent() function checks whether ch is a valid identifier character (e.g., a letter or underscore). Since charAt() now returns 'x9=9a9l9e9r9t9(919)', the comparison is based on the Unicode values of the characters in the string. As a result:
'a' (with a lower Unicode value) is considered less than 'x9=9a9l9e9r9t9(919)'.
'z' (with a higher Unicode value) is considered greater than 'x9=9a9l9e9r9t9(919)'.
This causes the isIdent() check to always return true, treating any string as a valid identifier and allowing potentially malicious expressions to bypass the sandbox.
Note
Note that we need to use AngularJS’s $eval() function because overwriting the charAt() function will only take effect once the sandboxed code is executed.
Constructing an Advanced AngularJS Sandbox Escape
A site may block the use of single or double quotes. In this case, you can use functions like String.fromCharCode() to generate characters. While AngularJS restricts access to the String constructor in expressions, you can bypass this by using the constructor property of a string instead.
In a typical sandbox escape, you’d use $eval() to execute a JavaScript payload, but in some situations, $eval() is undefined. Luckily, we can use the orderBy filter instead. The usual syntax for the orderBy filter is:
[123] | orderBy: 'Some string'
Note that the | operator here is a filter in AngularJS, not a bitwise OR as in JavaScript. In this example, the array [123] is passed to the orderBy filter, with the string 'Some string' as its argument. While orderBy is typically used to sort objects, it can also evaluate expressions, allowing us to use it to pass a payload.
Lab: Reflected XSS with AngularJS Sandbox Escape without Strings
The application has the following client-side script that utilizes the concept of controllers and $parse service:
Control the data of AngularJS application and they are just regular JavaScript objects.
Here’s a simple example:
<!DOCTYPE html><html><head> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script></head><body><div ng-app="myApp" ng-controller="myCtrl"> <!-- Binding a simple expression --> <p>{{ greeting }} {{ name }}!</p></div><script> // Define an AngularJS module and controller var app = angular.module('myApp', []); app.controller('myCtrl', function($scope) { $scope.greeting = "Hello"; $scope.name = "World"; });</script></body></html>
Explain:
{{ greeting }} {{ name }} is the AngularJS template expression. It dynamically evaluates the values of greeting and name from the $scope and displays them.
When rendered in the browser, this will display Hello World!.
$parse Service
The $parse service in AngularJS is a function that takes an expression string and returns a function that can be used to parse and evaluate the expression. The expression string can contain variables, operators, and function calls.
To use the $parse function, it must first be injected as a dependency into an AngularJS component, such as a controller. Here’s how you can inject it into a controller:
angular.module('myModule', []).controller('MyController', function($scope, $parse) { // Use the $parse service here});
Syntax of $parse:
var fn = $parse(expression);var result = fn(context, locals);
For example, when passing the expression 1+1 to the $parse function, it will generate a function like this:
And the expression lies below the script will render the $scope.value property, which will execute the payload via the invocation made on the $parse() function:
<h1 ng-controller="vulnCtrl" class="ng-scope ng-binding">10 search results for {{value}}</h1>
AngularJS CSP Bypass
Depending on the policy, the CSP may block JavaScript events. However, AngularJS defines its own events that can be used instead. When inside an event handler, AngularJS provides a special $event object, which refers to the browser’s event object.
In Chrome, the $event object has a path property, which is an array of objects that trigger the event. The last item in this array is always the window object, which can be used to escape the sandbox. By passing this array to the orderBy filter, we can access the window object and use it to execute global functions like alert(). The following code demonstrates this:
The from() function is used to convert an object into an array and then apply a function (specified in the second argument) to each element. Here, we’re using it to call the alert() function. We can’t call alert() directly because the AngularJS sandbox would detect the use of the window object. By using from(), we hide the window object from the sandbox, allowing us to inject malicious code.
There are several ways to hide the window object from the AngularJS sandbox. One method is using the array.map() function like this:
[1].map(alert);
The map() function calls alert() for each item in the array, bypassing the sandbox because it uses the alert() function without directly referencing window.
Lab: Reflected XSS with AngularJS Sandbox Escape and CSP
We can not invoke the alert() function directly so we need to assign it to a variable (z) and then invoke that variable.
Finally, the id=x combines with #x fragment is an alternative way of focusing the element without using the autofocus attribute. This way, we can reduce the number of characters in the payload.
Info
The current instance of the lab has HttpOnly attribute on the cookie so we can not alert the cookie locally. However, it may be works on the victim side.
How to Prevent Client-side Template Injection Vulnerabilities
To prevent client-side template injection vulnerabilities, avoid using untrusted user input to generate templates or expressions. If this isn’t possible, filter out template expression syntax from user input before embedding it.
HTML encoding alone isn’t enough to prevent these attacks, as frameworks decode the content before processing template expressions.