I just started learning something about web app pentesting and I was trying my hand on a website (which I'm allowed to test on). I found this potential stored XSS:

<div title="hello">hello</div>

Here, the div content (which is retrieved from a back end request) is copy-pasted to the title attribute of the div itself. When I try to change the div content via back end request to something like

"><img src=x onerror='alert(1)'>

it won't pop the XSS. Upon further investigation, I found that when I copied the HTML from the developer console (on Chrome, don't know if it matters) I'll then paste something like this:

<div title="&quot;><img src=x onerror='alert(1)'>">"&gt;&lt;img src=x onerror='alert(1)'&gt;</div>

enter image description here

Now, since the content comes from a backend response exactly as I wrote it (image below), the escaping must come from the frontend then.

enter image description here

Is there any way to bypass this kind of encoding?

The frontend should be implemented using Preact. Is it automatically escaping strings? Please let me know if I should provide more details

EDIT: clarifications

  • 1) You have done good work. Actually this is a potential XSS problem in the backend application, because a safe application should not return any plain script or plain markup in the data. All data containing script and markup should be escaped. 2) Without knowing what code you have in the frontend it is impossible to say at what stage using what technique it was escaped. Analyze frontend code.
    – mentallurg
    Jun 5, 2020 at 18:42
  • As general suggestions for the future it is best to avoid screenshots and instead format it as code. You can use single backticks for inline code and triple backticks for code blocks. Also you do have to be careful when copying out of the developer console. Data in the "inspect element" tool has been processed by Chrome and this can hide what is going on. It's better to stick to the "View Page Source" (which is the "raw" data) or the raw request and response in the network tab. Jun 6, 2020 at 10:26
  • Thank you both for your answers! @mentallurg I'll be checking the frontend code in depth as my next step :) ConorMancone thanks for the suggestion! Unfortunately, the page is rendered dynamically using different requests, thus I'm not able to find what I'm looking for within the source code
    – apontini
    Jun 6, 2020 at 10:37
  • 1
    @mentallurg This is not true. While XSS escaping is often done on the backend, it technically should happen on the front end. In fact since all modern JavaScript frameworks (react, angular, etc) handle XSS automatically, if you were using such a framework you can't escape on the backend because then, for instance, your &amp; will be displayed as exactly that instead of an actual &. Jun 6, 2020 at 10:39
  • @ConorMancone you're then saying that my initial idea was correct (i.e. Preact is escaping string by its own)? Which path should I take then to try and see if bypassing this encoding is possible?
    – apontini
    Jun 6, 2020 at 10:43

1 Answer 1


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:


<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");
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="&quot;><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 &quot 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": "&quot;Awesome!&quot;"}

When we hover we'll see a title of &quot;Awesome!&quot; 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="&amp;quot;Awesome!&amp;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");
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.


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...

  • Long answers are the best answers! Thank you, you’ve been clear and exhaustive :)
    – apontini
    Jun 8, 2020 at 6:40

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .