0

In creating a simple demonstration of XSS I'm failing to see the result of the injected code, and I'm wondering what simple thing I've overlooked.

This is the code that handles the AJAX request after the user presses enter in the specified input element:

document.getElementById('xssVulnerable')
.addEventListener( 'keyup', function(event) {
    if (event.key === "Enter") {
        let msg = document.getElementById('xssVulnerable').value;
        fetch('xssVulnerable.php', {
            method: "POST",
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify( {'userInput':msg} )
        })
        .then( response => response.text() )
        .then( data => {
            document.getElementById('xssEcho').innerHTML = data;
        });
    }
});

I originally had this using GET but had suspected that the url-encoding of the parameters was making some subtle difference. To attempt to avoid that I've used POST body instead. Removing the Content-type header makes no difference to the results.

Network communication showing sent data

Network communication showing received data

The HTML excerpt is trivial, and does show the echoed output correctly, unless I provide a string with a script tag. I.e. if I input <strong>Hi</strong> I will see a bolded Hi inserted. If I use <script>console.log('hi');</script> I see no output, including nothing in the console but I can see the injected tag through the element browser.

<input type="text" id="xssVulnerable">
<p id='xssEcho'></p>

DOM showing injected trivial JavaScript code

The server side response is similarly trivial, by intent:

<?php
    $body = json_decode(file_get_contents('php://input'), true);
    echo $body['userInput'];
?>

Double checking the documentation I found no evidence that the .text() method performs any sanitization, and neither is there any automatic sanitization by the reading of the .value property nor by the use of .innerHTML The rendering of the strong tag exemplifies that the code is being re-rendered on receipt, which is as expected. Neither did I find any reason why a script tag could not be nested inside a paragraph tag.

I accept that this is not a traditional XSS attack and that a more relevant example would be if the string were read from a file or database and output without sanitization, but the goal is to demonstrate the problem in a simple, introductory manner.

I tested this in current versions of Chrome and Edge, though I'm also unaware of any behavior these browsers might have to suppress a dynamically injected script.

What nuance have I missed?

1
  • stackoverflow.com/a/35462561/759749 demonstrates a work around where you need to create a script object and copy the script into it to get the script to execute. Since the answer is circa 2016 I expect this has been a long standing attempt to make the browsers support more resilience towards XSS. Useful for anyone hoping to add a script by ajax call, even if that's a highly unusual/risky use case.
    – Stephen
    Commented Aug 28, 2023 at 3:21

2 Answers 2

2

Double checking the documentation I found no evidence that the .text() method performs any sanitization, and neither is there any automatic sanitization by the reading of the .value property nor by the use of .innerHTML

It's there, you just didn't read closely enough, or perhaps not far enough down.

From https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML#security_considerations:

Although this may look like a cross-site scripting attack, the result is harmless. HTML specifies that a <script> tag inserted with innerHTML should not execute.

For the actual standard, linked from the above excerpt, see https://www.w3.org/TR/2008/WD-html5-20080610/dom.html#innerhtml0 (specifically, the green text):

Note: script elements inserted using innerHTML do not execute when they are inserted.


As far as I know, this is only applicable to <script> tags, and not the other ways to insert script into a page. For example, using the developer console on Firefox, document.body.innerHTML += '<script>alert(1)</script>' doesn't trigger an alert, but document.body.innerHTML += '<iframe src="javascript:parent.alert(2)"></iframe>' does, as does document.body.innerHTML += '<img src="qq" onerror="alert(3)">'.

Also, you should not rely on this for protection, even in cases where you're very sure it'll be a <script> tag and not something else. There are some innocuous-seeming operations that will cause the browser to re-parse the relevant portion of the DOM, at which point script tags set with innerHTML may well execute. I don't recall the details - it's a rare case where this matters, and I learned it years ago, so this might be irrelevant now - but some DOM manipulation functions de facto cause the browser to re-parse sections of the DOM and this can be used to, in effect, trigger DOM-based XSS in cases like this.

1
  • 1
    You're quite right that I missed the discussion on the non-execution of the script tag. The innerHTML does not sanitize but the script does not run anyway by specification, hence the warning about using .setHTML().
    – Stephen
    Commented Aug 28, 2023 at 18:37
0

Based on this answer I modified my code to do the following:

document.getElementById('xssVulnerable')
.addEventListener( 'keyup', function(event) {
    if (event.key === "Enter") {
        let msg = document.getElementById('xssVulnerable').value;
        fetch('xssVulnerable.php', {
            method: "POST",
            body: JSON.stringify( {'userInput':msg} )
        })
        .then( response => response.text() )
        .then( data => {
            let container = document.getElementById('xssEcho');
            container.innerHTML = '';

            let newdiv = document.createElement('div');

            // https://stackoverflow.com/a/35462561/759749
            // this makes the example highly convoluted, 
            // but it's for demonstration only.
            
            if ( data.toLowerCase().indexOf('<script>') >= 0 ) {
                let script = document.createElement('script');
                script.innerHTML = 
                         data.substring( 
                          data.toLowerCase().indexOf('<script>')+8, 
                          data.toLowerCase().lastIndexOf('</script>')
                         );
                newdiv.appendChild(script);
            } else {
                newdiv.innerHTML = data;
            }

            container.appendChild(newdiv);
        });
    }
});

As I mention in my comment this is inelegant, but it seems I'm circumventing normal DOM loading rules. There may be a better method or clearer explanation but this is functional.

You must log in to answer this question.

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