Knockout.js: HtmlNoScript binding
The Knockout.js is one of the most popular and fast-developing libraries that bring the MVVM pattern into the world of JavaScript development. Knockout.js allows to declaratively bind UI (Document Object Model elements) to an underlying data model. One of the built-in bindings is the html-binding allowing to display an arbitrary Html provided by an associated property/field of the data model. The typical use of this binding may look like this:
<div data-bind="html: comment" />
where the comment property may be specified as shown in the following data model:
... // include the knockout library <script src="js/knockout/knockout-2.2.0.debug.js" type="text/javascript"></script> ... $(function () { // define a data model var viewModel = function () { var self = this; self.comment = ko.observable("some html comment <span>Hello<script type='text/javascript'>alert('Hello!');<\/script></span>"); ... } ... // create instance of the data model var viewModelInstance = new viewModel(); // bind the instance to UI (since that moment all of the declarative bindings have worked off and the data are displayed) ko.applyBindings(viewModelInstance); });
Applying a value to a DOM element, the html-binding exploits the jQuery‘s html() method, of course, if jQuery is available on the page; otherwise knockout.js relies on its own logic that ends up with call of the DOM‘s appendChild function. In both cases if the html being applied contains inclusions of JavaScripts, the scripts execute once the html has been added to the DOM. Preventing all unwanted scripts from running is the best practice when displaying random htmls on the page. Using the approach described in my post How to prevent execution of JavaScript within a html being added to the DOM we are able to develop our own knockout binding disabling the scripts. It could look like the following:
ko.bindingHandlers.htmlNoScripts = { init: function () { // Prevent binding on the dynamically-injected HTML return { 'controlsDescendantBindings': true }; }, update: function (element, valueAccessor, allBindingsAccessor) { // First get the latest data that we're bound to var value = valueAccessor(); // Next, whether or not the supplied model property is observable, get its current value var valueUnwrapped = ko.utils.unwrapObservable(value); // disable scripts var disarmedHtml = valueUnwrapped.replace(/<script(?=(\s|>))/i, '<script type="text/xml" '); ko.utils.setHtml(element, disarmedHtml); } };
An alternative way to prevent scripts from running is to use the innerHTML property of a DOM element as follows:
var html = "<script type='text/javascript'>alert('Hello!');<\/script>"; var newSpan = document.createElement('span'); newSpan.innerHTML = str; document.getElementById('content').appendChild(newSpan);
Some people, however, report that this doesn’t work in Internet Explorer 7 for unknown reasons. So, let’s combine these two methods in one to minimize the risk of unwanted scripts running. My tests show the combined approach successfully prevent JavaScript from being executed in all popular browsers. Even in the worst case, at least one of the methods works off. The resultant knockout binding may look like the following:
ko.bindingHandlers.htmlNoScripts = { init: function () { // Prevent binding on the dynamically-injected HTML return { 'controlsDescendantBindings': true }; }, update: function (element, valueAccessor, allBindingsAccessor) { // First get the latest data that we're bound to var value = valueAccessor(); // Next, whether or not the supplied model property is observable, get its current value var valueUnwrapped = ko.utils.unwrapObservable(value); // disable scripts var disarmedHtml = valueUnwrapped.replace(/<script(?=(\s|>))/i, '<script type="text/xml" '); // create a wrapping element var newSpan = document.createElement('span'); // safely set internal html of the wrapping element newSpan.innerHTML = disarmedHtml; // clear the associated node from the previous content ko.utils.emptyDomNode(element); // add the sanitized html to the DOM element.appendChild(newSpan); } };
This htmlNoScripts binding can be used as follows:
<div data-bind="htmlNoScripts: comment" />