Skip to main content

Solving the XSS problem by signing <SCRIPT> tags

Last week I talked about JavaScript security at Virus Bulletin 2009. One of the security problems with JavaScript (probably the most insidious) is Cross-site Scripting (which is usually shortened to XSS).

The basic defense against XSS is to filter user input, but this has been repeatedly shown to be a nightmare. Just yesterday Reddit got hit by an XSS worm that created comments because of a bug in the implementation of markdown.

I believe the answer is for sites to sign the <SCRIPT> tags that they serve up. If they signed against a key that they control then injected JavaScript could be rejected by the browser because its signature would be missing or incorrect and the entire XSS problem would disappear.

For example, this site includes Google Analytics and here's the JavaScript:

<script type="text/javascript">
var gaJsHost = (("https:" == document.location.protocol) ?
"https://ssl." : "http://www.");
document.write(unescape("%3Cscript src='" + gaJsHost +
"google-analytics.com/ga.js'
type='text/javascript'%3E%3C/script%3E"));
</script>

<script type="text/javascript">
try {
var pageTracker = _gat._getTracker("UA-402747-4");
pageTracker._trackPageview();
} catch(err) {}</script>

Since I chose to include that JavaScript I could also sign it to say that I made that decision. So I could modify it to something like this:

<script type="text/javascript"
sig="068dd60b18b6130420fed77417aa628b">
var gaJsHost = (("https:" == document.location.protocol) ?
"https://ssl." : "http://www.");
document.write(unescape("%3Cscript src='" + gaJsHost +
"google-analytics.com/ga.js'
type='text/javascript'%3E%3C/script%3E"));
</script>

<script type="text/javascript"
sig="17aa628b05b602e505b602e505b602e5">
try {
var pageTracker = _gat._getTracker("UA-402747-4");
pageTracker._trackPageview();
} catch(err) {}</script>

The browser could verify that everything between the <SCRIPT> and </SCRIPT> is correctly signed. To do that it would need access to some PK infrastructure. This could be achieved either by piggybacking on top of existing SSL for the site, or by a simple scheme similar to DKIM where a key would be looked up via a DNS query against the site serving the page.

For example, jgc.org could have a special TXT DNS entry for _scriptkey.jgc.org which would contain the key for signature verification.

To make this work correctly with externally sourced scripts it would be important to include the src attribute in the signature. Or alternatively an entirely new tag just used for signatures could be created to sign the HTML between the tags:

<sign sig="068dd60b18b6130420fed77417aa628b">
<script type="text/javascript">
var gaJsHost = (("https:" == document.location.protocol) ?
"https://ssl." : "http://www.");
document.write(unescape("%3Cscript src='" + gaJsHost +
"google-analytics.com/ga.js'
type='text/javascript'%3E%3C/script%3E"));
</script>

<script type="text/javascript">
try {
var pageTracker = _gat._getTracker("UA-402747-4");
pageTracker._trackPageview();
} catch(err) {}</script>
</sign>

Either way this would mean that JavaScript blocks could be signed against the site serving the JavaScript completely eliminating XSS attacks.

Note that in the case of externally sourced scripts I am not proposing that their contents be signed, just that the site owner sign the decision to source a script from that URL. This means that an XSS attack isn't possible. Of course if the remotely sourced script itself is compromised there's still a problem, but it's a different problem.

Comments

martijn said…
Isn't the new Firefox going to have something similar to SPF, where websites can say from which sources they would expect JS -- the idea, I believe, is to get rid of inline-JS and that JS code should only be loaded from external sources.

I like the idea of signing JS, but I wonder what happens if the webmaster (or one of the webmasters) of a smaller site unknowingly adds a space, removes a line break or simply saves the page under a different encoding.
Anonymous said…
You'd have to be more careful about how you sign the tags, to that it's not vulnerable to the length-extension attack that was just found in Flickr's API. http://vnhacker.blogspot.com/2009/09/flickrs-api-signature-forgery.html
Anonymous said…
I apologize for not having tried this before suggesting it, but couldn't the first script that runs on the page attach a hook that does this validation before other scripts on the page run? And, should the validation fail, remove the script before the browser executes it (say, by removing all attributes and emptying the body).

per martijn's comments about line breaks: any such scheme should consider normalization first. the negative side of normalization is that it increases overhead for signature verification. But you could normalize away empty lines, many form of whitespace, even names of local variables if you so desired -- anything that you can prove won't change the "meaning" of the text. In the case of a normal programming language, anything that won't change the parse tree or how symbol names will resolve (though in any program where eval can directly or indirectly be called, symbol names can affect program semantics, so you might want to think twice about normalizing locals in that case)
Andy said…
What happens if an already signed JavaScript is exploited?

For instance a script is dynamically created on the server side and is signed on the server; however some piece of unfiltered user input was placed in that generated script.
Kamil Szot said…
Better yet sanitize your output not the input. If you are using PHP then use htmlspecialchars() on any data before gluing it to string containing HTM).

Or perhaps there is some way of sneaking script tag into your site if you are converting all < to < before outputting them?

Popular posts from this blog

Your last name contains invalid characters

My last name is "Graham-Cumming". But here's a typical form response when I enter it:


Does the web site have any idea how rude it is to claim that my last name contains invalid characters? Clearly not. What they actually meant is: our web site will not accept that hyphen in your last name. But do they say that? No, of course not. They decide to shove in my face the claim that there's something wrong with my name.

There's nothing wrong with my name, just as there's nothing wrong with someone whose first name is Jean-Marie, or someone whose last name is O'Reilly.

What is wrong is that way this is being handled. If the system can't cope with non-letters and spaces it needs to say that. How about the following error message:

Our system is unable to process last names that contain non-letters, please replace them with spaces.

Don't blame me for having a last name that your system doesn't like, whose fault is that? Saying "Your last name …

All the symmetrical watch faces (and code to generate them)

If you ever look at pictures of clocks and watches in advertising they are set to roughly 10:10 which is meant to be the most attractive (smiling!) position for the hands. They are actually set to 10:09.14 if the hands are truly symmetrical. CC BY 2.0image by Shinji
I wanted to know what all the possible symmetrical watch faces are and so I wrote some code using Processing. Here's the output (there's one watch face missing, 00:00 or 12:00, because it's very boring):



The key to writing this is to figure out the relationship between the hour and minute hands when the watch face is symmetrical. In an hour the minute hand moves through 360° and the hour hand moves through 30° (12 hours are shown on the watch face and 360/12 = 30).
The core loop inside the program is this:   for (int h = 0; h <= 12; h++) {
    float m = (360-30*float(h))*2/13;
    int s = round(60*(m-floor(m)));
    int col = h%6;
    int row = floor(h/6);
    draw_clock((r+f)*(2*col+1), (r+f)*(row*2+1), r, h, floor(m…

Importing an existing SSL key/certificate pair into a Java keystore

I'm writing this blog post in case anyone else has to Google that. In Java 6 keytool has been improved so that it now becomes possible to import an existing key and certificate (say one you generated outside of the Java world) into a keystore.

You need: Java 6 and openssl.

1. Suppose you have a certificate and key in PEM format. The key is named host.key and the certificate host.crt.

2. The first step is to convert them into a single PKCS12 file using the command: openssl pkcs12 -export -in host.crt -inkey host.key > host.p12. You will be asked for various passwords (the password to access the key (if set) and then the password for the PKCS12 file being created).

3. Then import the PKCS12 file into a keystore using the command: keytool -importkeystore -srckeystore host.p12 -destkeystore host.jks -srcstoretype pkcs12. You now have a keystore named host.jks containing the certificate/key you need.

For the sake of completeness here's the output of a full session I performe…