Safe version of jQuery’s .html()

var data = {
	url: 'http://w3.org/',
	user: {
		firstName: 'John',
		lastName: 'O\'Connor'
	}
};

var html = '<a href="' + data.url +'" alt="' + data.user.firstName + ' ' + data.user.lastName + '">'
	+ data.user.firstName
	+ '</a>';

$(element).html(html);
	

Looks familiar?

When developer needs to insert small piece of HTML to page, usually he uses element.innerHTML or jQuery.fn.html, and result of it Cross-Site Scripting (XSS).

Attacker can insert malicious script to page which can steal data like session, credit card information, passwords and then trying to send spam and recopy itself.

But usually developer don’t need to insert scripts with innerHTML, and also it not allowed by Content Security Policy (CSP), but because of lack of native JavaScript functions which can properly encode user data to HTML entities, developer can make HTML injection vulnerability which is main cause of XSS attack. Because of most user data doesn’t contain special characters, developers and even testers don’t see any difference. That why this kind of vulnerability is so popular today.

My solution is create special function which doing all this job by default. And also provide useful interface to insert any user data to HTML.
{{param}} — param will be HTML encoded;
{{{htmlEncodedParam}}} — if param already HTML encoded, we can prevent double encoding;

var template = '<a href="{{url}}" title="{{user.firstName}} {{user.lastName}}">{{{i18n}}}</a>';
$(element).staticHTML(template, data);
	

It much easier to read and maintain, it works very fast because of using regular expressions and it prevents all known kinds of XSS attack.

“implementation seems frustratingly strong ;)”
— Mario Heiderich

Also it fixes vulnerability in jQuery 1.8.x.

var name = '<img src=xx: onerror=alert(domain)>';
	
XSS.jQueryStrict = false;	// report mode
$('a[name="' + name + '"]');

XSS.jQueryStrict = true;	// strict mode
$('a[name="' + name + '"]');
	

Redefine XSS.report to send full report to server. localStorage.setItem('XSS.debug', 1) to enable debug mode.

Allowed tags: a, abbr, area, audio, b, bdi, bdo, big, blockquote, br, button, cite, code, del, dfn, div, em, font, form, h1, h2, h3, h4, h5, h6, hr, i, img, input, ins, kbd, label, li, map, mark, marquee, nobr, ol, optgroup, option, p, pre, q, rp, rt, ruby, s, samp, select, small, source, span, strike, strong, sub, sup, table, tbody, td, textarea, tfoot, th, thead, time, tr, u, ul, var, video, wbr

Not allowed tags: html, head, title, base, isindex, link, meta, style, script, noscript, body

Allowed attributes: accept, action, alt, border, checked, class, clear, color, cols, colspan, controls, coords, data, datetime, dir, disabled, enctype, for, frameborder, headers, height, hidden, href, hreflang, id, ismap, label, lang, loop, marginheight, marginwidth, max, maxlength, method, min, multiple, name, pattern, placeholder, preload, readonly, rel, required, reversed, rows, rowspan, sandbox, scrolling, seamless, size, spellcheck, src, start, step, target, title, type, typography, usemap, value, width

Demo

Try to change HTML template and data.

var element = document.getElementById('element');
var template = document.getElementById('source').value;
var data = JSON.parse(document.getElementById('data').value);
$(element).staticHTML(template, data);
	
HTML Template JSON Data
Rendered HTML Preview
DOM
Tests
Results

Also read “An Introduction to Content Security Policy