IT IS HERE! Get Smashing Node.JS on Amazon Kindle today!
Show Posts
← Back to homepage

Two nights ago I was editing my Digg profile and couldn’t help but think about the recent Mikeyy and Twitter revolt. Within minutes I had found a XSS exploit that could theoretically allow me to achieve the same.

Half an hour later, I had a working worm ready to infect everyone that saw my profile page, which also would propagate to theirs.

Locating the vulnerability

XSS vulnerabilities are quite ironic nowadays. Everyone knows about them, almost everyone knows what their most frequent form is, but few seem to test for them or implement systems to avoid them.

The most common form of a XSS exploit scenario is that in which the attacker manages to store malicious code in the host, which when shown is not escaped properly and is thus executed. On Digg, all it took me was to insert a <script> tag in the “About me” textarea.

Less known forms of XSS exploits involve other pathways through which the user sends information:

  • Request headers. A malicious user, like Al Capone or Mikeyy, could alter the User Agent or Referer, for example, to store malicious code.
  • Query string. Commonly known as the PHP_SELF bug, the vulnerable sites are those which print out the request URL without escaping it. For example, if a programmer decides to populate the a <form action=""> with the unsanitized request URL, an attacker could inject code that can be conveniently distributed with URL shortening services.

It’s also important to remember that preventing XSS exploits is not about escaping < or > alone. I recommend checking out this cheatsheet for a comprehensive list of possible, real-world attacks.

Exploiting it

The exploit code is only anecdotally interesting. I do not encourage anyone to start testing random websites and injecting harmful JavaScript.

The attack plan is as follows

  1. Check the user is logged in, by checking for the absence of a login link.

    if ($('#header-login').attr('href')) return;
    	
  2. Propagate the script by retrieving the profile edit details form and ajaxly submitting it.

    $.ajax({url: 'http://digg.com/settings/about', dataType: 'html', success: function(doc){
    	// ...
    	$.ajax({url: 'http://digg.com/settings/about', data: form.serialize(), type: 'POST'});
    	// ...
    }});
    	
  3. We disable the Digg Bar by posting to http://digg.com/settings/viewing. This time we don’t really need to retrieve the whole form, because we already have the magic token that prevents CSRF attacks
  4. We shout to friends to check out our profile. When a friend is infected, the <script> tag also holds the information of the scripter, so that the victim doesn’t shout back, which could raise suspicions that something fishy is going on.
  5. We also store the created shouts ids in a cookie, to hide them from the user view as soon as the script loads and avoid users seeing weird shouts they didn’t voluntarily send.

    var shouts = $.cookie('shouts'), shoutedTo = [], shoutIds = [];
    if (shouts) {
    	shouts = shouts.split(',');
    	$(shouts).each(function(i, code){
    		code = code.split('|');
    		shoutIds.push(code[0]); shoutedTo.push(code[1]);
    		document.write('');
    	});
    	document.write('');
    }
    	

Additionally, I set up a callback page and a very basic form of cross-domain requests (using images) ((Keep in mind that, for security reasons, XMLHttpRequest would not allow me to communicate with my server from digg.com. I thus used (new Image).src to ping the script I set up.)) to inform me of the success of the different stages of the worm (infection, bar disabling, propagation) for each user.

Check out the full script here.

Preventing it

First of all, it’s important to constantly keep in mind that user-supplied information should be untrusted. Whenever you input or output information that was originated by a 3rd party, extra caution should be taken.

For PHP developers, preventing XSS exploits usually means passing all user-supplied info through the htmlentities() function. The caution part is not a cliche, or a meaningless recommendation. It might be obvious for you, as a developer, to escape $_POST['value'] but escaping $_SERVER['HTTP_USER_AGENT'] takes undoubtedly more attention.

The Symfony framework provides one of the arguably most elegant solutions to this problem, with its built-in XSS protection, virtually transparent to the developer. Learn more about this method.

Conclusion

I enabled the worm on Digg to try it out with my friends, and it quickly began to spread. After succeeding and collecting about 20 users, I quickly commented it out and let the Digg crew know. Jen Burton, the community manager, responded really quickly, and within an hour or two they had fixed the bug and updated the production site.

20 Comments

Lewis said

Wow, I’m surprised Digg failed such basic validation. Just as a note, I’d recommend using htmlspecialchars instead of htmlentities to avoid problems with charsets and encodings. It’s just as effective at stopping XSS but it’s less obtrusive.

InternetUser said

Yet again somewhat surprising, but props to Digg for taking quick action. Props on your ethical approach vs kiddies/Mikeyy. You have a bright future!

Claude said

Nice job Guillermo.

You mentioned that the Symfony framework does a good job of sanitizing input to avoid XSS exploits. Have you tried CodeIgniter’s XSS filtering, and if so, what’s your take on it?

Thanks.

    Guillermo Rauch said

    Apparently it should be the same, given

    $config['global_xss_filtering'] = TRUE;

    is enabled.

Your thoughts?

About Guillermo Rauch:

CTO and co-founder of LearnBoost / Cloudup (acquired by Automattic in 2013). Argentine living in SF.