Using HTML 5 to Defeat XSS
Cross site scripting (XSS), also known as arbitrary script injection, is one of the most pervasive, and pernicious, vulnerabilities facing modern web applications. The folks who have worked on the HTML 5 specification, and the browser manufacturers who have implemented it, have tackled the very laudable goal of eliminating XSS at the client side. While not fool proof, this is an amazing step towards eliminating injected script in web applications and removes much of the burden for sanitizing user supplied input from the authors of web applications and places that burden on the client.
The XSS Problem
Script injection occurs any time an attacker can manipulate the output HTML of a web application in such a way as to craft markup. This markup is usually a chunk of JavaScript, but it could really be anything. Of course, clients trust the HTML they render from a web server, and assume that all scripts and mark up in HTML supplied by a server were authored by developers for that server. Unfortunately this isn't always the case. Attackers have found clever ways to insert malicious markup in pages using all sorts of mechanisms, but most commonly via URL variables and user supplied input stored in back-end databases. When users view pages that utilize these polluted resources then markup crafted by attackers, rather than web developers, renders in the client's browser.
The problem for browsers is how to distinguish between safe, and unsafe mark up. If all mark up comes from the same source it's tough to discern what might be an attacker supplied iframe tag that references a malware domain from an iframe rendered by a legitimate advertising partner of a website. What's more, the browser has no way to tell if JavaScript referenced from remote URL's is to be trusted or not.
The Solution
HTML 5 introduces the concept of Content Security Policy (CSP). CSP is defined in HTTP header directives delivered from the server. Using these policies it is possible for application authors to establish some baselines about what can, and should not, be trusted by the client. In this way, even if an attacker can inject some malicious script, unless it conforms to the defined CSP (or the attacker can manipulate the CSP) then the browser will ignore that script.
It should go without saying that if an attacker can get in the middle of a connection and alter the stream (such as with a classic man in the middle (MITM) attack) they can simply eliminate, or alter, HTTP headers that define the CSP. By using HTTPS applications can avoid this problem to a great extent, but communications over HTTP may still be vulnerable to attack. It is important to realize this limitation of CSP.
The cornerstone of properly enforced CSP is the idea that the baseline policy disallows any inline JavaScript from executing. This is both wonderful, and a hassle. It is good architectural policy to separate data from instructions, and removing inline JavaScript from your HTML will make it more readable in maintainable. It may also be a headache in the short term as you adjust to the new model.
Demonstration
Let's look at a typical example of a XSS attack. The following HTML represents the response from a server's search function that might be delivered to a client, but contains a malicious chunk of JavaScript which performs the canonical alert box attack:
<!DOCTYPE HTML> <html> <head> <title>HTML 5 Search Results</title> <body> <h1>Search Results</h1> <p> Your search for: <script>alert('xss');</script> returned <em>0</em> results. </p> </body> </html>
This might result if a user searched for the string of JavaScript and the subsequent results page failed to sanitize the user supplied input. The result is predictable:
Now consider the application of a content security policy. To do this you need some way to set HTTP response headers, such as the PHP header function. With the following chunk of PHP we can set this CSP:
<?php header("Content-Security-Policy: default-src 'self'"); header("X-Content-Security-Policy: default-src 'self'"); ?> <!DOCTYPE HTML> <html> <head> <title>HTML 5 Search Results</title> <body> <h1>Search Results</h1> <p> Your search for: <script>alert('xss');</script> returned <em>0</em> results. </p> </body> </html>
Rendered in a modern HTML 5 compliant browser you'll notice that the alert box does not render. Using the Firebug plugin we can even see that the browser reports it has blocked the malicious piece of script.
While the effect is observable, the cause may bear some explanation. I should first address the fact that there are two headers set instead of one, and that they vary slightly. The reason for this is that Firefox prior to version 23 requires the X-Content-Security-Policy header which is now deprecated (https://developer.mozilla.org/en-US/docs/Security/CSP/Introducing_Content_Security_Policy). If you test this code on other browsers you can eliminate the second header and simply use the first one, which is standards compliant (https://dvcs.w3.org/hg/content-security-policy/raw-file/tip/csp-specification.dev.html).
CSP, like all headers, is simply a key value pair separated by colons. The policy statement key is stock. The value in this example is the base minimum, to supply a default source for all dynamic content. HTML 5 relies on an expansion of the Same Domain Origin policy of HTML 4. Although it allows for expansion of origin, domains must be strictly defined. Domains can be listed as full URL's, or using the keywords 'none', 'self', or the two unsafe keywords that can be used to basically undo the policy 'unsafe-inline' and 'unsafe-eval' (more on why you would do this later). In this example we've specified 'self' meaning that scripts can be loaded from any source associate with the current domain of the script (the top level domain, excluding any subdomains, but including any subdirectories: so example.com would forbid sub.example.com but allow example.com/scripts). Remember that all inline scripts are forbidden by the CSP, so even though we allow loading of scripts, inline scripts fail to render.
The advantage of the CSP is that we defeat inline scripts, but we can still load scripts from JavaScript files in the allowed domains. For instance, if we wanted to allow some dynamic functionality, say an alert box when you click on the 'Search Results' title, you can still facilitate that feature. You merely have to separate out your JavaScript into a file, then set event handlers in that JavaScript to carry out the dynamic code. So, where in the past, you might do this using code such as:
<h1 onclick="javascript:alert('You clicked the title!');">
You can now use the HTML above, with the addition of an id attribute to the header tag, and create a new stuff.js page containing the following code which adds an event handler when the page has loaded:
function doStuff() { alert('You clicked the title!'); } document.addEventListener('DOMContentLoaded', function () { document.getElementById('alertTitle') .addEventListener('click', doStuff); });
You can include this code in your header and everything should work fine with the CSP:
<?php header("Content-Security-Policy: default-src 'self'"); header("X-Content-Security-Policy: default-src 'self'"); ?> <!DOCTYPE HTML> <html> <head> <title>HTML 5 Search Results</title> <script type="text/javascript" src="/stuff.js"></script> <body> <h1 id="alertTitle">Search Results</h1> <p> Your search for: <script>alert('xss');</script> returned <em>0</em> results. </p> </body> </html>
Now users can click on the title and view the alert, but the inline script is disabled.
Detection
One amazing advantage of using the new CSP is that you can not only prevent XSS, but you can also detect it. Traditionally XSS attacks occur at the client side and the ability for the server, and developers, to detect the attack is severely limited since they cannot observe the browser as it renders code (unless it's the browser of the developer). CSP includes the extremely useful feature of providing reporting whenever a script is blocked. This is primarily useful when debugging and migrating applications to the use of the new CSP. In fact, this is one reason you might want to use the 'unsafe-inline' policy, in conjunction with reporting so you can track where browsers would block scripts if the full policy was used.
CSP reporting posts JSON objects to a report URI that is defined in the HTTP header. For instance, the following directive instructs the browser to report any errors that might occur to the URL report.php on the same server:
header("Content-Security-Policy: default-src 'self'; report-uri /report.php");
Capturing these reports is straightforward using PHP. The following code will write them out to a text log file on the server:
<?php // report.php $file = fopen('csp-report.txt', 'a'); $json = file_get_contents('php://input'); $csp = json_decode($json, true); foreach ($csp['csp-report'] as $key => $val) { fwrite($file, $key . ': ' . $val . " "); } fwrite($file, 'End of report.' . " "); fclose($file); ?>
This log file is then easy for developers to read, and additionally can be tied into security event tracking systems to alert administrators when application clients are being attacked via XSS (assuming their browser blocks the XSS and reports it properly, which most modern browsers should do):
document-uri: http://10.0.0.99/search-results.php referrer: blocked-uri: self violated-directive: inline script base restriction source-file: http://10.0.0.99/search-results.php script-sample: alert('xss'); line-number: 8 End of report.
Conclusion
Although HTML 5 is often the whipping boy of information security gurus, there are a number of security enhancements that are easily overlooked. Content Security Policy is one of the more revolutionary aspects of HTML 5 from a security perspective. If properly understood and utilized HTML 5 can greatly enhance the security posture of most web applications.
Using HTML 5 to defeat, and detect, XSS attacks is simple and easy. Of course, it may require the re-architecture of some JavaScript elements in applications so is likely going to be of most use in the development of new applications. Legacy apps and frameworks may take some time to refactor so they can use the CSP, but the reporting feature can help to ease this burden a great deal.