A Journey With Trusted HTML in AngularJS

Wednesday, September 10, 2014

AngularJSAngularJS  provides protection against cross-site scripting attacks by default.  Let’s look at some examples.

HTML Entity Encoding With ngBind

Consider the following controller which maintains a collection of strings with HTML inside. Perhaps the strings arrived in the controller as the result of an HTTP call, or perhaps the controller built the strings dynamically. In either case, the controller wants the links to display as clickable navigation links in a browser.

app.controller("mainController", function() {
    var main = this;

    main.links = [
        "<a href='http://google.com'>Google</a>",
        "<a href='http://odetocode.com'>OdeToCode</a>",
        "<a href='http://twitter.com'>Twitter</a>",
    ];
});

In the view, we’ll use a {{bind expression}} to display the links.

<section ng-controller="mainController as main">

    <nav>
        <ul>
            <li ng-repeat="link in main.links">{{link}}</li>
        </ul>
    </nav>

</section>

HTML Encoded with ngBind The links will appear as inert text (see the figure to the right), because interpolation with ngBind will produce encoded text.

Technically, this isn’t because of any special code inside of the ngBind directive, but because ngBind uses jqLite’s .text method, which ultimately sets the textContent property of the associated element.

Render HTML with ngBindHtml

Instead of using ngBind, the view can use ngBindHtml to render the links as real, clickable anchor tags.

<ul>
    <li ng-repeat="link in main.links"
        ng-bind-html="link">
    </li>
</ul>

However, making only this change in the view will result in an error.

Error: [$sce:unsafe] Attempting to use an unsafe value in a safe context.

Angular will only render “safe” HTML into the DOM. If you are looking for a quick fix to this error, just read the “Sanitized HTML” section below.

From a developer’s perspective, there are two general categories of “safe” HTML in Angular.

  1. Explicitly trusted HTML is safe
  2. Sanitized HTML is safe

Let’s look at #2 first.

Sanitizing HTML

Synonyms for sanitize include “sterilize”, “disinfect”, “clean”, “cleanse”, and “purify”. All of these synonyms are capable words for describing the process of making HTML “safe” for display. Being “safe” means the HTML won’t carry script code into the DOM, because script code is dangerous if the script comes from the wrong place.

AngularJS includes a $sanitize service that will parse an HTML string into tokens and only allow safe and white-listed markup and attributes to survive, thus sterilizing a string so the markup contains no scripting expressions or dangerous attributes.

Angular will consider HTML processed by $sanitize as trusted.

The ngBindHtml directive will use the $sanitize service implicitly, however, the $sanitize service is not part of the core ng module, so an additional script needs to be added into the page.

<script src="angular.js"></script>
<script src="angular-sanitize.js"></script>
<script src="app.js"></script>

Then when registering the application module, the code must list ngSantize as a dependency.

var app = angular.module("app", ["ngSanitize"]);

Render sanitized HTML with ngBindHtml With these changes in place, the app will now render proper links.

So what was sanitized?

Watching $sanitize

One way to see how and when $sanitize works is to decorate the $sanitize service (which is a function object).

var app = angular.module("app", ["ngSanitize"]);

app.config(function($provide){
    $provide.decorator("$sanitize", function($delegate, $log){
        return function(text, target){

            var result = $delegate(text, target);
            $log.info("$sanitize input: " + text);
            $log.info("$sanitize output: " + result);

            return result;
        };
    });
});

Now let’s use the following link collection inside the controller. Notice each link has some attribute present.

app.controller("mainController", function() {

    var main = this;
    main.links = [
        "<a onmouseover='...' href='http://google.com'>Google</a>",
        "<a class='...' href='http://odetocode.com'>OdeToCode</a>",
        "<a ng-click='...' href='http://twitter.com'>Twitter</a>"
    ];
});

With the decorator, we can see exactly what goes into $sanitize, and what comes out. The mouseover attributes disappear, as does the ng-click directive.

$sanitize input: <a onmouseover='...' href='http://google.com'>Google</a> 
$sanitize output: <a href="http://google.com">Google</a> 

$sanitize input: <a class='...' href='http://odetocode.com'>OdeToCode</a>
$sanitize output: <a class="..." href="http://odetocode.com">OdeToCode</a>

$sanitize input: <a ng-click='...' href='http://twitter.com'>Twitter</a>
$sanitize output: <a href="http://twitter.com">Twitter</a> 

While $sanitize can provide trusted HTML for Angular to render, it does so by modifying the HTML. If we really want to render HTML without encoding and without sanitization, we have to turn to what I referred to as category #1 earlier – explicitly marking the HTML as “trusted”.

Explicitly Trusting HTML With $sce

When you want Angular to render model data as HTML with no questions asked, the $sce service is what you’ll need. $sce is the Strict Contextual Escaping service – a fancy name for a service that can wrap an HTML string with an object that tells the rest of Angular the HTML is trusted to render anywhere.

In the following version of the controller, the code asks for the $sce service and uses the service to transform the array of links into an array of trusted HTML objects using $sce.trustAsHtml.

app.controller("mainController", function($sce) {

    var main = this;
    main.links = [
        "<a onmouseover='alert(\"careful!\")' href='http://google.com'>Google</a>",
        "<a href='http://odetocode.com'>OdeToCode</a>",
        "<a href='http://twitter.com'>Twitter</a>"
    ];

    for (var i = 0; i < main.links.length; i++) {
        main.links[i] = $sce.trustAsHtml(main.links[i]);
    }
});

The only difference between this version of the controller code and the previous version is the for loop to mark the strings as trusted. When binding to each trusted link using ngBindHtml, the HTML will no longer go through the $sanitize service. This means the first link will now enter the DOM with an onmouseover attribute and carry along executable JavaScript code. Be careful!

Bonus: Compiling Trusted HTML

Instead of using onmouseover or onclick attributes, let’s say we have some strings on our controller that want to follow the “Angular Way” and use ngClick or ngMouseover.

app.controller("mainController", function($sce, $log) {

    var main = this;
    main.links = [
        "<a ng-click='main.go(\"google\")' href=''>Google</a>",
        "<a ng-click='main.go(\"otc\")' href=''>OdeToCode</a>",
        "<a ng-click='main.go(\"twitter\")' href=''>Twitter</a>"
    ];

    for (var i = 0; i < main.links.length; i++) {
        main.links[i] = $sce.trustAsHtml(main.links[i]);
    }

    main.go = function(name){
       $log.info("Goto: " + name);
    };
});

If these links pass through the $sanitize service, the sanitizer will remove ng-click and other attribute directives. Fortunately, the controller marks the HTML as trusted so they will render with ngClick. However, clicking will not work since Angular doesn’t compile the markup after it enters the DOM with ngBindHtml.

The solution is to use a custom directive in combination with $sce and trusted HTML.

app.directive("compileHtml", function($parse, $sce, $compile) {
    return {
        restrict: "A",
        link: function (scope, element, attributes) {

            var expression = $sce.parseAsHtml(attributes.compileHtml);

            var getResult = function () {
                return expression(scope);
            };

            scope.$watch(getResult, function (newValue) {
                var linker = $compile(newValue);
                element.append(linker(scope));
            });
        }
    }
});

Now the view looks like:

<ul>
    <li ng-repeat="link in main.links"
        compile-html="link">
    </li>
</ul>

Comments
Michael Wednesday, September 10, 2014
Comment (required) (will not accept < or > symbols, so no markup (sorry!)): Maybe the comments section needs to use some sanitation :P
gravatar Scott Wednesday, September 10, 2014
@Michael: :) I keep meaning to switch to markdown... on my todo list.
Comments are closed.

My Pluralsight Courses

K.Scott Allen OdeToCode by K. Scott Allen
What JavaScript Developers Should Know About ECMAScript 2015
The Podcast!