Learning should always be fun

Risks of using innerHTML and outerHTML

Risks of using innerHTML and outerHTML

Hello gud ppl! In this tutorial we will  see the risks of using innertHTML and outerHTML and inject javascript as string in a HTML page. You must have known to insert html element via innertHTML or outerHTML property which works fine for inserting html elements but when it comes to scripts innerHTML will not work.

 

Lets get started…

Before we begin we will see what we are going to do. We all know that we have event callback function for html elements like onload, onerror etc. These script attributes can help us to inject javascripts to our page. But there is one flaw here. If the scripts are in the form of tags then we cannot use this method. In this case we capture all the attributes from the script tags and create a new script element with these attributes.

Create index.js as following:

 

/**
 *
 * @param {string} scriptStr - The scripts in string to be injected
 * @description
 * generateScriptsHTML genereate a tag that can be injected to the html page which can execute scripts
 * of the scriptStr string.
 * - When only JS is there img elements onerror callback is used to execute the scripts
 * - When script tag is also present then a new script element is crated and inserted in the head
 */
function generateScriptsHTML(scriptStr) {
    const patt = /<script.*?>((.|\n)*?)<\/script>/igm;
    const htmlStr = scriptStr
    var result = null;
    var startIndex = 0;// current start index of the recent scrip
    var endIndex = 0;// current end index of the recent script
    var resultHtml = "";
    while (result = patt.exec(htmlStr)) {
        const outerHTML = result[0];
        const innerHTML = result[1];
        endIndex = result.index
        var otherHtml = htmlStr.substring(startIndex, endIndex) // other tag than script
        var attributes = getAttributes(result[0])
        if (doesAttributeContains(attributes, "src")) { // insert the script tag in head
            var head = document.getElementsByTagName('head')[0];
            var script = document.createElement('script');
            attributes.forEach(attribute => {
                if (attributes == "async") script.async = true;
                else script[attribute.key] = attribute.val;
            })
            head.appendChild(script);
            resultHtml += otherHtml
        } else { // we need to now inject this script
            resultHtml += otherHtml
                + `<img src="errimg" style="display:none" onerror="${innerHTML}"></img>`
        }
        startIndex = endIndex + outerHTML.length
    }
    resultHtml += htmlStr.substring(startIndex, htmlStr.length)
    return resultHtml
}


// get the attributes of the scripts
function getAttributes(data) {
    const patt = /<script.*?( .*?)>/igm
    let result = null
    let scripts = []
    result = patt.exec(data)
    const attributes = []
    if (!result) return attributes

    if (result.length > 1) {
        const rawAttributes = result[1].split(" ")
        rawAttributes.forEach(item => {
            const rawAttribute = item.split("=")
            if (rawAttribute[0].trim() == "") return;
            const attribute = {
                key: rawAttribute[0].trim(),
                val: rawAttribute.length > 1 ? rawAttribute[1].trim().replace(/\"/g, "") : 0
            }
            attributes.push(attribute)
        })
    }
    if (attributes.length != 0) scripts.push(attributes);
    return attributes
}

function doesAttributeContains(attributes, key) {
    for (const attribute of attributes) {
        if (attribute.key == key) return true
    }
    return false
}

 

Now create index.html as :

 

<html>

<head>
    <title>Inject scripts</title>
</head>

<body>
    <div id="inject_container"></div>
    <div>
        <textarea id="scripts_container" placeholder="Script here.."></textarea>
    </div>
    <button id="inject_btn">Inject</button>
</body>
<script src="index.js"></script>
<script>

    let
        injectBtn = null,
        injectContainer = null,
        scriptContainer = null;

    function init() {
        injectBtn = document.getElementById("inject_btn");
        scriptContainer = document.getElementById("scripts_container");
        injectContainer = document.getElementById("inject_container");

        injectBtn.onclick = injectTextAreaScript;
    }

    function injectTextAreaScript() {
        console.log("onclick :: ",scriptContainer.value);
        const scriptHTMLStr = generateScriptsHTML(scriptContainer.value);
        console.log("script ::"+scriptHTMLStr);
        const divElem = document.createElement("div");
        divElem.innerHTML = scriptHTMLStr;
        injectContainer.appendChild(divElem);
    }

    window.onload = init()
</script>

</html>

 

Now run open index.html in your browser and paste some inline scripts in the text area. I used twitter scripts and can successfully inject it.

 

All we have done is inserted the scripts in the onerror callback of img tag. In order to do so we set error image src in the tag which will fire the onerror event. When we have script tag present we extract the attributes like src and create a script tag which is appended to the head tag.

 

Conclusion

The code above may not work in some scripts. But the point I want to show you guys is it’s very risky to use innerHTML. Even though we cannot use innertHTML to inject script, we can have a workaround like above to do so.

 

Reference:

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

 

Github link: https://github.com/AbhijetPokhrel/injectJS