tl/dr: Most likely there is no exploit here. The behavior of the backend (passing down unescaped data) is perfectly normal and
acceptable, as escaping has to happen on the frontend. Moreover,
escaping on the frontend most likely is happening, but the behavior
of the Inspect Element tool in Chrome can confuse that issue.
Overview of examples
Both these issues can be quite complicated so I'm going to show a number of examples to demonstrate what is happening. To have some useful examples you need both a backend and frontend. However, I'm not actually going to have a backend so that you can easily reproduce it yourself. Instead I'm just going to have some static files that act like a backend, and I'm going to use some simple JavaScript to both demonstrate proper frontend security and how Chrome can confuse the issue if you aren't familiar with some key details. The following examples make actual HTTP requests so you'll have to have these files hosted by an webserver - if you try doing this with local files (aka File -> Open) Chrome will complain about CORS issues that you can't fix.
Safely setting HTML attributes
Anyway we're going to start with an example that is not vulnerable to XSS. Here's an HTML file with some javascript:
index.html:
<script type="text/javascript">
var oReq = new XMLHttpRequest();
oReq.addEventListener("load", function(){
var test_element = document.getElementById('secure');
var response = JSON.parse(this.responseText);
test_element.setAttribute('title', response.name)
});
oReq.open("GET", "response.json");
oReq.send();
</script>
Test: <span id="secure" title="">Secure</span>
response.json
is just a plain text file in the same folder as the above HTML file and contains:
{"name": "\"><img src=x onerror='alert(1)'>"}
Therefore when the page loads it makes an HTTP request for some JSON (which includes your payload), parses it, and then sets its contents as the title of our span. Despite a valid XSS payload no alert will fire. If you hover over the span you'll see your payload, and if you inspect element you'll see this:
<span id="secure" title=""><img src=x onerror='alert(1)'>">Secure</span>
Which looks like it should have worked, but it clearly didn't. That's just because of the way Chrome shows such content though. If you right click the span in the "Inspect Element" window and click "Edit as HTML" it will change to:
<span id="secure" title=""><img src=x onerror='alert(1)'>">Secure</span>
Which you can see is perfectly secure. There was no vulnerability. Chrome confused the issue because in the "Inspect Element" window it showed the "
as an actual "
. The reason it is secure is because, in essence, there are many "safe" methods available in JavaScript that leave no room for XSS. For these methods Chrome itself automatically applies the proper "escaping". Using these safe methods is the equivalent of using prepared queries with SQL, and as long as this is what the frontend is doing, you have no hopes of an XSS vulnerability (or at least if you did find an XSS vulnerability for one of these safe methods you would have a high-value browser 0-day and could get tens of thousands of dollars from Google for it).
When you don't understand what Chrome is doing though it can be very confusing, because in the inspect element tool it looks like your exploit worked. It didn't of course, the inspect element tool just shows the escaped characters as their replacements because... well... that's just how it is made.
A note about React, Angular, et al....
Further, note that modern frontend frameworks like React and Angular know which methods are safe and which methods aren't and use the safe methods exclusively. As a result the risk of XSS vulnerabilities are effectively zero when using modern frontend frameworks "properly".
A note about escaping on the backend
Note that this is also why it's important not to escape on the backend. Imagine there is no payload, a user used an actual quote in the data, and we escaped it when we sent it down. As a result we'll change our response to:
{"name": ""Awesome!""}
When we hover we'll see a title of "Awesome!"
and if we inspect element and then Edit as HTML to see what the browser is actually doing we'll see:
<span id="secure" title="&quot;Awesome!&quot;">Secure</span>
AKA the browser escaped our already escaped data, leading to double escaping and a really weird result for users. This is not what we want, and would be the equivalent of adding slashes to user input before using prepared queries - it creates more of a mess and is unnecessary.
This is why it's not a problem that the response with your payload looks unescaped - it should be. Otherwise the application would end up with double escaped data and that would confuse people.
Safely setting tag content
So what would vulnerable code look like? Let's change our code to show another safe method, except instead of changing the title let's change the actual tag contents, so we'll change our index.html to:
<script type="text/javascript">
var oReq = new XMLHttpRequest();
oReq.addEventListener("load", function(){
var test_element = document.getElementById('secure');
var response = JSON.parse(this.responseText);
test_element.textContent = response.name;
});
oReq.open("GET", "response.json");
oReq.send();
</script>
Test: <span id="secure" title="">Secure</span>
And our response.json to:
{"name": "<img src=x onerror='alert(1)'>"}
When the page loads you'll see:
Test: <img src=x onerror='alert(1)'>
With no alert.
Insecurely changing tag contents
Now let's live dangerously. Change just one line of javascript:
test_element.innerHTML = response.name;
Then reload the page and you will see your alert. XSS Success!
The difference is that the innerHTML
method isn't safe. It takes the content and renders it as actual HTML, which means that any HTML in the content will actually render/execute as HTML. In this case you would want to escape your data on the backend before sending it down if it isn't supposed to be HTML. Of course that would be a dangerous proposition because if your backend forgot to escape the data then you would end up with an XSS vulnerability. It's much safer to use safe methods exclusively.
Summary
I realize that this is quite a long answer :) My goal here is to give some understanding about what is happening "under the hood" so you can both understand why Chrome is giving confusing results, and also understand when an application may or may not be vulnerable. That's quite simple: as long as the application is using "safe" methods to update the DOM then you won't manage an XSS attack anymore than you could execute an SQL payload on a site using prepared queries. Of course the hard part is determining how the application is handling input and therefore whether or not there is a vulnerability. Theoretically the JavaScript is available to you so you could always read it and find out, although in practice that is rarely easy...
&
will be displayed as exactly that instead of an actual&
.