Guest Post: Adam Barth on Three simple rules for building XSS-free web applications

| Comments (2) | SYSSEC
Today I had occasion to ask Web Security expert Adam Barth about avoiding XSS vulnerabilities. He was kind enough to help me out and (since he's presumably tired of answering this kind of question) also to write up some guidance for the rest of the world. His answer is below:

Folks often ask me how to build web applications without cross-site scripting (XSS) vulnerabilities, but I haven't really been able to find a reference that I'd be happy recommending. There are, of course, many different approaches you can use to build an XSS-free web application. This guide recommends a simplistic approach that works well for "single-document" web applications (like Gmail and Twitter) that use a single HTML document for the lifetime of the application.

Because complexity is the enemy of security, we approach the problem of eliminating XSS by simplifying how we handle untrusted data (by which we mean any information we retrieve from the network or from the DOM):

  1. Untrusted data MUST NOT be transmitted in the same HTTP responses as HTML or JavaScript. In particular, the main HTML document SHOULD be static (and therefore cacheable for a long time).

  2. When transmitted from the server to the client, untrusted data MUST be properly encoded in JSON format and the HTTP response MUST have a Content-Type of application/json.

  3. When introduced into the DOM, untrusted data MUST be introduced using one of the following APIs:
    • Node.textContent
    • document.createTextNode
    • Element.setAttribute (second parameter only)

That's it. If you follow those three rules, you stand a good chance of avoiding XSS. These rules are conservative. You can certainly build a secure web site that violates one or more of these rules. Conversely, these rules don't guarantee success. For example, they don't stop you from doing some dumb things:

// The textContent of a <script> element is active content.
var scriptElement = document.createElement('script');
scriptElement.textContent = userData.firstName;  // XSS!
document.body.appendChild(scriptElement);

// Some attributes (mostly event handlers) are active content:
var imageElement = document.createElement('img');
document.body.appendChild(imageElement);
imageElement.setAttribute('onload', userData.lastName);  // XSS!
imageElement.src = 'http://example.com/logo.png';

However, common sense should help you avoid those situations.

2 Comments

This post is all over the place.

For #1: What does cacheability have to do with XSS attacks?

For #2: Why would a server ever provide untrusted, non-sanity-checked data?

For #3: is there a XSS vulnerability when the "untrusted" data that is being introduced into the DOM, without using one of the three routines described in the post, came from the local host? If not, the rule is too draconian.

#2: You make it sound like it's so easy to "sanity check" your data. Check out this paper http://www.cs.berkeley.edu/~prateeks/papers/scriptgard-ccs11.pdf that talks about how hard it is to apply the correct sanitizers to the right data. If you've gotten data from an untrusted source, why not always treat it like it's still untrusted...just in case?

#3: Sure. LocalStorage permits stored XSS attacks where the data appears to be "coming from" the local host (although the original source of data was remote).

Leave a comment