Lab 3: DOM XSS in innerHTML sink using source location.search
This lab contains a DOM-based cross-site scripting vulnerability in the blog search. It takes data from
location.searchand writes it into the page viainnerHTML.To solve the lab, perform a cross-site scripting attack that calls
alert.
Pre-amble, before the link to the lab. There is this text below, which tells essentially what we need to do and what will or won’t work.
The
innerHTMLsink doesn’t execute<script>in modern browsers, andsvg onloadwon’t fire here. Use alternative elements likeimgoriframewith event handlers (e.g.,onerror,onload).Example:
element.innerHTML='... <img src=1 onerror=alert(document.domain)> ...'
Initial Reconnaissance / Discovery:
Logging onto the blog we can see there is a search function at the top of the page.
Let’s enter a unique string bl00dsti113r and observe the results.
We can see our search string is being directly passed as a paramter in the url
/?search=bl00dsti113r
In DevTools, we see the value is written once into the page under the element with id searchMessage.
Analyzing the Source Code / Behavior:
Let’s take a look at the source code and see what is happening.
<script>
function doSearchQuery(query) {
document.getElementById('searchMessage').innerHTML = query;
}
var query = (new URLSearchParams(window.location.search)).get('search');
if (query) {
doSearchQuery(query);
}
</script>
doSearchQuery Function
function doSearchQuery(query) {
document.getElementById('searchMessage').innerHTML = query;
}
This finds the element with id searchMessage and sets its innerHTML to whatever value query contains.
+In simple terms+: It replaces the contents of searchMessage with the value of query as HTML.
If we look further up the page we can see the target element searchMessage on the page.
Query Variable
var query = (new URLSearchParams(window.location.search)).get('search');
URLSearchParams reads the page’s query string (window.location.search).
.get('search') extracts the value of the search parameter (what we type into the search box).
This is the HTML for the search box. The input’s name is search, and the form uses GET, so your text is sent as a query parameter ?search=....
<section class=search>
<form action=/ method=GET>
<input type=text placeholder='Search the blog...' name=search>
<button type=submit class=button>Search</button>
</form>
</section>
+In simple terms+: Whatever we type in the search box goes into the URL as ?search=... and is stored in query.
Running the Function
if (query) {
doSearchQuery(query);
}
If query has a value, the page writes it into searchMessage via innerHTML.
Summary: Where the data comes from (Source) and where it goes (Sink)
- Source (attacker-controlled): The value in the page URL, e.g.
?search=...- Read by:
URLSearchParams(window.location.search).get('search')→ stored inquery
- Read by:
- Sink (dangerous write):
document.getElementById('searchMessage').innerHTML = query
+In plain English+: Whatever we type into the search box ends up in the URL, gets read into query, and is then inserted into the page as HTML. Because it isn’t encoded or sanitized, the browser can treat our input like code. That’s DOM XSS.
Flow:
URL ?search=... → query → innerHTML(#searchMessage) ⇒ code can run
Exploitation:
Because innerHTML is used with untrusted (unsanitized) input, we can inject an element with an event handler.
Steps:
- Use an element that can fire handlers when it fails, in this case we will use
<img ... onerror=...>. - Now we force an error by specifying a fake
srcimage so that the error triggers:<img src=1 onerror= - Now we specify an alert should be triggered in the event of an error
alert(1). - Final Payload :
<img src=1 onerror=alert(1)>
Place it in the URL parameter (URL-encode if needed, in this case it is not needed):
/?search=%3Cimg%20src%3D1%20onerror%3Dalert(1)%3E
Result: the alert fires and the lab is solved.
After dismissing the alert, you’ll see the broken image indicator because the file doesn’t exist.
Why This Is Vulnerable:
Primary issue: Untrusted input from location.search is inserted into the DOM via innerHTML without encoding/sanitization.
How to Fix (Safer Patterns):
How To Fix: OWASPS Guidelines on making dynamic updates to the DOM recommend:
RULE #1 - HTML Escape then JavaScript Escape Before Inserting Untrusted Data into HTML Subcontext within the Execution Context
There are several methods and attributes which can be used to directly render HTML content within JavaScript. These methods constitute the HTML Subcontext within the Execution Context. If these methods are provided with untrusted input, then an XSS vulnerability could result.
HTML encoding & also JavaScript encoding all untrusted input.
Guidelines Taken from OWASP :
//Dangerous
element.innerHTML = "<HTML> Tags and markup";
//Safer with User Input being encoded
var ESAPI = require('node-esapi');
element.innerHTML = "<%=ESAPI.encoder().encodeForJavascript(ESAPI.encoder().encodeForHTML(untrustedData))%>";
For general other guidance, see OWASP’s DOM-based XSS Prevention Cheat Sheet: