Executing <script> elements inserted with .innerHTML

186,546

Solution 1

The OP's script doesn't work in IE 7. With help from SO, here's a script that does:

exec_body_scripts: function(body_el) {
  // Finds and executes scripts in a newly added element's body.
  // Needed since innerHTML does not run scripts.
  //
  // Argument body_el is an element in the dom.

  function nodeName(elem, name) {
    return elem.nodeName && elem.nodeName.toUpperCase() ===
              name.toUpperCase();
  };

  function evalScript(elem) {
    var data = (elem.text || elem.textContent || elem.innerHTML || "" ),
        head = document.getElementsByTagName("head")[0] ||
                  document.documentElement,
        script = document.createElement("script");

    script.type = "text/javascript";
    try {
      // doesn't work on ie...
      script.appendChild(document.createTextNode(data));      
    } catch(e) {
      // IE has funky script nodes
      script.text = data;
    }

    head.insertBefore(script, head.firstChild);
    head.removeChild(script);
  };

  // main section of function
  var scripts = [],
      script,
      children_nodes = body_el.childNodes,
      child,
      i;

  for (i = 0; children_nodes[i]; i++) {
    child = children_nodes[i];
    if (nodeName(child, "script" ) &&
      (!child.type || child.type.toLowerCase() === "text/javascript")) {
          scripts.push(child);
      }
  }

  for (i = 0; scripts[i]; i++) {
    script = scripts[i];
    if (script.parentNode) {script.parentNode.removeChild(script);}
    evalScript(scripts[i]);
  }
};

Solution 2

Simplified ES6 version of @joshcomley's answer with an example.

No JQuery, No library, No eval, No DOM change, Just pure Javascript.

http://plnkr.co/edit/MMegiu?p=preview

var setInnerHTML = function(elm, html) {
  elm.innerHTML = html;
  Array.from(elm.querySelectorAll("script")).forEach( oldScript => {
    const newScript = document.createElement("script");
    Array.from(oldScript.attributes)
      .forEach( attr => newScript.setAttribute(attr.name, attr.value) );
    newScript.appendChild(document.createTextNode(oldScript.innerHTML));
    oldScript.parentNode.replaceChild(newScript, oldScript);
  });
}

Usage

$0.innerHTML = HTML;    // does *NOT* run <script> tags in HTML
setInnerHTML($0, HTML); // does run <script> tags in HTML

Solution 3

Here is a very interesting solution to your problem: http://24ways.org/2005/have-your-dom-and-script-it-too

So it would look like this instead:

<img src="empty.gif" onload="alert('test');this.parentNode.removeChild(this);" />

Solution 4

You should not use the innerHTML property but rather the appendChild method of the Node: a node in a document tree [HTML DOM]. This way you are able to later call your injected code.

Make sure that you understand that node.innerHTML is not the same as node.appendChild. You might want to spend some time on the Javascript Client Reference for more details and the DOM. Hope the following helps...

Sample injection works:

<!DOCTYPE HTML>
<html>
<head>
    <title>test</title>
    <script language="javascript" type="text/javascript">
        function doOnLoad() {
            addScript('inject',"function foo(){ alert('injected'); }");
        }
    
        function addScript(inject,code) {
            var _in = document.getElementById('inject');
            var scriptNode = document.createElement('script');
            scriptNode.innerHTML = code;
            _in.appendChild(scriptNode);
        }
    </script>
</head>
<body onload="doOnLoad();">
    <div id="header">some content</div>
    <div id="inject"></div>
    <input type="button" onclick="foo(); return false;" value="Test Injected" />
</body>
</html>

Solution 5

Here's a shorter, more efficient script that also works for scripts with the src property:

function insertAndExecute(id, text) {
    document.getElementById(id).innerHTML = text;
    var scripts = Array.prototype.slice.call(document.getElementById(id).getElementsByTagName("script"));
    for (var i = 0; i < scripts.length; i++) {
        if (scripts[i].src != "") {
            var tag = document.createElement("script");
            tag.src = scripts[i].src;
            document.getElementsByTagName("head")[0].appendChild(tag);
        }
        else {
            eval(scripts[i].innerHTML);
        }
    }
}

Note: whilst eval may cause a security vulnerability if not used properly, it is much faster than creating a script tag on the fly.

Share:
186,546
phidah
Author by

phidah

Updated on January 31, 2022

Comments

  • phidah
    phidah over 2 years

    I've got a script that inserts some content into an element using innerHTML.

    The content could for example be:

    <script type="text/javascript">alert('test');</script>
    <strong>test</strong>
    

    Problem is that the code inside the <script> tag doesn't get executed. I googled it a bit but there were no apparent solutions. If I inserted the content using jQuery $(element).append(content);the script parts got eval'd before being injected into the DOM.

    Has anyone got a snippet of code that executes all the <script> elements? The jQuery code was a bit complex so I couldn't really figure out how it was done.

    Edit:

    By peeking into the jQuery code I've managed to figure out how jQuery does it, which resulted in the following code:

    Demo:
    <div id="element"></div>
    
    <script type="text/javascript">
      function insertAndExecute(id, text)
      {
        domelement = document.getElementById(id);
        domelement.innerHTML = text;
        var scripts = [];
    
        ret = domelement.childNodes;
        for ( var i = 0; ret[i]; i++ ) {
          if ( scripts && nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) {
                scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] );
            }
        }
    
        for(script in scripts)
        {
          evalScript(scripts[script]);
        }
      }
      function nodeName( elem, name ) {
        return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase();
      }
      function evalScript( elem ) {
        data = ( elem.text || elem.textContent || elem.innerHTML || "" );
    
        var head = document.getElementsByTagName("head")[0] || document.documentElement,
        script = document.createElement("script");
        script.type = "text/javascript";
        script.appendChild( document.createTextNode( data ) );
        head.insertBefore( script, head.firstChild );
        head.removeChild( script );
    
        if ( elem.parentNode ) {
            elem.parentNode.removeChild( elem );
        }
      }
    
      insertAndExecute("element", "<scri"+"pt type='text/javascript'>document.write('This text should appear as well.')</scr"+"ipt><strong>this text should also be inserted.</strong>");
    </script>
    
    • Andreas
      Andreas about 14 years
      Have your tried adding content (JS as innerHTML of DOM Node) and then calling the function(s) added? For example if you append Javascript containing a function FOO(){ } you can try calling function later.
    • Andreas
      Andreas about 14 years
      I don't think that you can have execution upon insertion in the DOM.
    • slugster
      slugster about 14 years
      Why can't you just iterate the children of the element, and for each one that is a script element you just eval() the innerHtml of that child? This is how i've seen it done by a large component vendor, every time they complete an ajax callback that adds stuff to the DOM they do exactly that. Bear in mind though that it can be slow, especially in IE7.
    • phidah
      phidah about 14 years
      Andreas: If I add a function, for example function testFunction(){ alert('test'); } to the code inserted into innerHTML, and then try calling it, it says that the function is not defined.
    • Andreas
      Andreas about 14 years
      Hello, i have posted an answer that you might find helpful..
    • Marcin
      Marcin over 11 years
      Awesome phidah, works like charm, cheers
    • Xatian
      Xatian about 7 years
      I think it is absolutely important to understand that this is intended behaviour by the browser to prevent Cross-site scripting attacks. If the text you set as innerHTML is provided by Bob it would execute on Alice's browser causing damage (think of a forum where people can write comments adding script-tags to them). You can read more about it here: en.wikipedia.org/wiki/Cross-site_scripting. Stay save!
    • marciowb
      marciowb over 4 years
      A HTML changed a lot since 2010. These present days, maybe you want look: stackoverflow.com/a/58862506/890357
    • DylanYoung
      DylanYoung about 4 years
      Because the node has already loaded. Can't you just put the deferred attribute on your script tag?
  • baptx
    baptx almost 12 years
    and even better, no need to have an image with "onerror" event, nice for quick XSS injection jvfconsulting.com/blog/47/… :)
  • iirekm
    iirekm about 10 years
    Better use jQuery's $(parent).html(code) - see my answer below.
  • S4beR
    S4beR over 9 years
    once script is injected to DOM, how should I remove it?
  • Jorge Fuentes González
    Jorge Fuentes González over 9 years
    Fast and pretty. Thank you.
  • John
    John over 9 years
    this helped me but i feel dirty using eval. making sure text cannot be compromised i don't see a vulnerability.
  • st4wik
    st4wik about 9 years
    The script isn't recursive, so will only look at direct children. This works for me: if (nodeName(child, "script" ) && (!child.type || child.type.toLowerCase() === "text/javascript")) { scripts.push(child); } else { exec_body_scripts(child); }
  • Savas Vedova
    Savas Vedova about 9 years
    You can use <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAA‌​AAALAAAAAABAAEAAAIBR‌​AA7" onload="alert('test');"> if you want to prevent a useless http request.
  • jonathanKingston
    jonathanKingston over 8 years
    @random-user eval was designed to hurt users. Any dynamic script execution is a risk and this is why CSP calls it 'unsafe-eval' because it is. You are also hurting the security of your sites if you are using it in a library as they can't turn it off.
  • Codewithcheese
    Codewithcheese over 8 years
    Testing this in Chrome 44 causes an infinite loop when appendChild is called since this increments the scripts.length value.
  • Ryan Morlok
    Ryan Morlok over 8 years
    Note that the above code doesn't execute scripts that load via src. The above script can be changed to check elem.src and conditionally set the src property of the created script element instead of setting its text content.
  • RiggsFolly
    RiggsFolly about 8 years
    Finally someone that actually explains a bit about the issue rather than all the other try this, look how clever I am answers. Deserves an UV, it got mine.
  • robert4
    robert4 about 8 years
    Scripts with the src property will be downloaded asynchronously and executed as arrived. Ordering is not preserved. Inline scripts will also be executed out-of-order, synchronously before the async ones.
  • wetlip
    wetlip over 7 years
    i uv this because it is the most simple way to inject javascript code that executes after injecting . I only dont grasp the difference between adding with innerHTML which doesn't execute, and the way above with appendChild which executes. I used this successfully to create a dynamic page with script from scratch with socket.io
  • Finesse
    Finesse about 7 years
    Why not to use the global eval trick instead of creating a <script> element and inserting it to the <head>? They both execute JS code without exposing the current closure.
  • kris
    kris about 7 years
    love it ! (added style="display:none;) to hide the broken image icon
  • Floris
    Floris over 6 years
    Thats it! Thank you.
  • Ron Burk
    Ron Burk over 6 years
    var _in = document.getElementById(inject);, I think.
  • pery mimon
    pery mimon over 5 years
    typo : the name of the function is setInnerHtml not setInnerHTML
  • pery mimon
    pery mimon over 5 years
    look like a genius idea
  • zavr
    zavr about 5 years
    yeah this method works but you'll get errors if you have comments or console.logs so watch out for that also you can modify to account for modules var modules = [] var cleaned = text.replace(/<script([^>]*)>([\s\S]*?)<\/script>/gi, function(m, tags, script){ if (/type="module"/.test(tags)) { modules.push(script) return } scripts += script + '\n' return '' })
  • basin
    basin about 5 years
    Actually, <style> is better than <img>, because it does not make a network request
  • danbars
    danbars almost 5 years
    Note that in the plnkr that is linked, the function name is setInnerHTML but it is being called setInnerHtml from within runB function. Therefore the example doesn't work
  • Manngo
    Manngo over 4 years
    What makes you say that it is easier? It’s not even shorter. I always think that overusing jQuery is a bad idea.
  • mplungjan
    mplungjan over 3 years
    newer invocation: [...document.querySelectorAll(`#${id} script`)].forEach(script => { if (scripts.src != "") { ... }})
  • Vizor
    Vizor about 3 years
  • MiBol
    MiBol almost 3 years
    I have an issue with VUE and the v-html where I want to inject some HTML with scripts... this help me out to fix my problem. stackoverflow.com/questions/68042540/…
  • todbott
    todbott almost 3 years
    Solved my problem after 4 hours of trial-and-error (using Springboot and Thymeleaf, by the way, and this still works).
  • Čamo
    Čamo almost 3 years
    Ok but I get an error: parameter 1 is not of type 'Node'.
  • Bestknighter
    Bestknighter over 2 years
    Yep, @basin is correct. I used <style onload="alert('test');"/> and it worked like a charm. No network request, minimal increase in document/request size, invisible... If you still want to remove it, you can also use the removeChild trick. Thanks!
  • like2think
    like2think almost 2 years
    I appreciate the compact code and still solved an issue today.