Ejecutando elementos <script> insertados con .innerHTML

111

Tengo un script que inserta contenido en un elemento usando innerHTML.

El contenido podría ser, por ejemplo:

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

El problema es que el código dentro de la <script>etiqueta no se ejecuta. Lo busqué en Google un poco pero no había soluciones aparentes. Si inserté el contenido usando jQuery, $(element).append(content);las partes del script se obtuvieron evalantes de inyectarse en el DOM.

¿Alguien tiene un fragmento de código que ejecuta todos los <script>elementos? El código de jQuery era un poco complejo, así que no pude entender cómo se hacía.

Editar :

Al echar un vistazo al código de jQuery, me las arreglé para descubrir cómo lo hace jQuery, lo que resultó en el siguiente código:

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>
phidah
fuente
1
¿Por qué no puede simplemente iterar los hijos del elemento, y para cada uno de los elementos de un script, simplemente evalúa () el innerHtml de ese hijo? Así es como lo he visto hacerlo un gran proveedor de componentes, cada vez que completan una devolución de llamada ajax que agrega cosas al DOM, hacen exactamente eso. Sin embargo, tenga en cuenta que puede ser lento, especialmente en IE7.
slugster
2
Andreas: Si agrego una función, por ejemplo function testFunction(){ alert('test'); }al código insertado en innerHTML, y luego intento llamarlo, dice que la función no está definida.
phidah
1
Phidah impresionante, funciona como un encanto, salud
Marcin
3
Creo que es absolutamente importante comprender que este es un comportamiento previsto por el navegador para evitar ataques de secuencias de comandos entre sitios. Si Bob proporciona el texto que establece como innerHTML, se ejecutaría en el navegador de Alice y causaría daños (piense en un foro donde la gente puede escribir comentarios agregando etiquetas de script). Puede leer más sobre esto aquí: en.wikipedia.org/wiki/Cross-site_scripting . ¡Mantente a salvo!
Xatian
1
Un HTML cambió mucho desde 2010. En estos días, tal vez quieras mirar: stackoverflow.com/a/58862506/890357
marciowb

Respuestas:

27

El script de OP no funciona en IE 7. Con la ayuda de SO, aquí hay un script que sí:

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]);
  }
};
Larry K
fuente
4
Mejor use jQuery's $(parent).html(code): vea mi respuesta a continuación.
iirekm
una vez que se inyecta el script en DOM, ¿cómo debo eliminarlo?
S4beR
1
El guión no es recursivo, por lo que solo verá a los hijos directos. Esto funciona para mí:if (nodeName(child, "script" ) && (!child.type || child.type.toLowerCase() === "text/javascript")) { scripts.push(child); } else { exec_body_scripts(child); }
st4wik
2
Tenga en cuenta que el código anterior no ejecuta scripts que se cargan a través de src. El script anterior se puede cambiar para verificar elem.srcy establecer condicionalmente la srcpropiedad del elemento de script creado en lugar de establecer su contenido de texto.
Ryan Morlok
¿Por qué no utilizar el evaltruco global en lugar de crear un <script>elemento e insertarlo en el <head>? Ambos ejecutan código JS sin exponer el cierre actual.
Finesse
31

@phidah ... Aquí hay una solución muy interesante a su problema: http://24ways.org/2005/have-your-dom-and-script-it-too

En su lugar, se vería así:

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

usuario447963
fuente
2
y aún mejor, no es necesario tener una imagen con el evento "onerror", agradable para una inyección rápida de XSS jvfconsulting.com/blog/47/… :)
baptx
11
Puede utilizar <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" onload="alert('test');">si desea evitar una solicitud http inútil.
Savas Vedova
1
quiéralo ! (agregado style="display:none;) para ocultar el ícono de imagen rota
kris
En realidad, <style>es mejor que <img>, porque no realiza una solicitud de red
cuenca
23

Aquí hay una secuencia de comandos más corta y eficiente que también funciona para secuencias de comandos con la srcpropiedad:

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);
        }
    }
}

Nota: si bien evalpuede causar una vulnerabilidad de seguridad si no se usa correctamente, es mucho más rápido que crear una etiqueta de script sobre la marcha.

DividedByZero
fuente
1
esto me ayudó pero me siento sucio usando eval. asegurándome de que el texto no se vea comprometido, no veo una vulnerabilidad.
John
@ random-user evalfue diseñado para dañar a los usuarios. Cualquier ejecución de script dinámico es un riesgo y es por eso que CSP lo llama 'unsafe-eval'porque lo es. También está perjudicando la seguridad de sus sitios si lo usa en una biblioteca, ya que no pueden desactivarlo.
jonathanKingston
Probar esto en Chrome 44 provoca un bucle infinito cuando se llama a appendChild, ya que esto incrementa el valor de scripts.length.
Codewithcheese
4
Los scripts con la srcpropiedad se descargarán de forma asincrónica y se ejecutarán cuando lleguen. El pedido no se conserva. Los scripts en línea también se ejecutarán fuera de orden, sincrónicamente antes que los asincrónicos.
robert4
21

No debe usar la propiedad innerHTML sino el método appendChild del Node: un nodo en un árbol de documentos [HTML DOM]. De esta manera, puede llamar más tarde a su código inyectado.

Asegúrese de entender que node.innerHTML no es lo mismo que node.appendChild . Es posible que desee dedicar algún tiempo a la referencia de cliente de Javascript para obtener más detalles y el DOM. Espero que lo siguiente ayude ...

La inyección de muestra funciona:

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

Saludos,

Andreas
fuente
2
Finalmente alguien que realmente explica un poco sobre el tema en lugar de todas las demás try this, look how clever I amrespuestas. Se merece un UV, consiguió el mío.
RiggsFolly
i uv esto porque es la forma más sencilla de inyectar código javascript que se ejecuta después de inyectar. Solo no entiendo la diferencia entre agregar con innerHTML que no se ejecuta, y lo anterior con appendChild que se ejecuta. Usé esto con éxito para crear una página dinámica con script desde cero con socket.io
wetlip
2
var _in = document.getElementById(inject);, Yo creo que.
Ron Burk
21

Versión ES6 simplificada de la respuesta de @ joshcomley con un ejemplo.

Sin JQuery, sin biblioteca, sin evaluación, sin cambio de DOM, solo Javascript puro.

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);
  });
}

Uso

$0.innerHTML = HTML;    // does *NOT* run <script> tags in HTML
setInnerHTML($0, HTML); // does run <script> tags in HTML
allenhwkim
fuente
1
error tipográfico: el nombre de la función setInnerHtmlno essetInnerHTML
pery mimon
Tenga en cuenta que en el plnkr que está vinculado, el nombre de la función es setInnerHTMLpero se llama setInnerHtmldesde dentro de la runBfunción. Por lo tanto, el ejemplo no funciona
danbars
16

Prueba este fragmento:

function stripAndExecuteScript(text) {
    var scripts = '';
    var cleaned = text.replace(/<script[^>]*>([\s\S]*?)<\/script>/gi, function(){
        scripts += arguments[1] + '\n';
        return '';
    });

    if (window.execScript){
        window.execScript(scripts);
    } else {
        var head = document.getElementsByTagName('head')[0];
        var scriptElement = document.createElement('script');
        scriptElement.setAttribute('type', 'text/javascript');
        scriptElement.innerText = scripts;
        head.appendChild(scriptElement);
        head.removeChild(scriptElement);
    }
    return cleaned;
};


var scriptString = '<scrip' + 't + type="text/javascript">alert(\'test\');</scr' + 'ipt><strong>test</strong>';
document.getElementById('element').innerHTML = stripAndExecuteScript(scriptString);
fantactuka
fuente
sí, este método funciona, pero obtendrá errores si tiene comentarios o console.logs, así que tenga cuidado con eso también puede modificar para tener en cuenta los módulos var modules = [] var clean = text.replace (/ <script ([^> ] *)> ([\ s \ S] *?) <\ / script> / gi, function (m, tags, script) {if (/type="module"/.test(tags)) {modules.push (script) return} scripts + = script + '\ n' return ''})
zavr
13
function insertHtml(id, html)  
{  
   var ele = document.getElementById(id);  
   ele.innerHTML = html;  
   var codes = ele.getElementsByTagName("script");   
   for(var i=0;i<codes.length;i++)  
   {  
       eval(codes[i].text);  
   }  
}  

Funciona en Chrome en mi proyecto.

Bruce
fuente
Rápido y bonito. Gracias.
Jorge Fuentes González
¡Eso es! Gracias.
Floris
7

Una solución sin usar "eval":

var setInnerHtml = function(elm, html) {
  elm.innerHTML = html;
  var scripts = elm.getElementsByTagName("script");
  // If we don't clone the results then "scripts"
  // will actually update live as we insert the new
  // tags, and we'll get caught in an endless loop
  var scriptsClone = [];
  for (var i = 0; i < scripts.length; i++) {
    scriptsClone.push(scripts[i]);
  }
  for (var i = 0; i < scriptsClone.length; i++) {
    var currentScript = scriptsClone[i];
    var s = document.createElement("script");
    // Copy all the attributes from the original script
    for (var j = 0; j < currentScript.attributes.length; j++) {
      var a = currentScript.attributes[j];
      s.setAttribute(a.name, a.value);
    }
    s.appendChild(document.createTextNode(currentScript.innerHTML));
    currentScript.parentNode.replaceChild(s, currentScript);
  }
}

Esto esencialmente clona la etiqueta de secuencia de comandos y luego reemplaza la etiqueta de secuencia de comandos bloqueada con la recién generada, lo que permite la ejecución.

Joshcomley
fuente
3

scriptNode.innerHTML = codeno funcionó para IE. Lo único que puede hacer es reemplazarlo scriptNode.text = codey funciona bien

Jorge
fuente
3

Es más fácil usar jquery en $(parent).html(code)lugar de parent.innerHTML = code:

var oldDocumentWrite = document.write;
var oldDocumentWriteln = document.writeln;
try {
    document.write = function(code) {
        $(parent).append(code);
    }
    document.writeln = function(code) {
        document.write(code + "<br/>");
    }
    $(parent).html(html); 
} finally {
    $(window).load(function() {
        document.write = oldDocumentWrite
        document.writeln = oldDocumentWriteln
    })
}

Esto también funciona con scripts que utilizan document.writey scripts cargados mediante srcatributo. Desafortunadamente, incluso esto no funciona con los scripts de Google AdSense.

iirekm
fuente
1
¿Qué te hace decir que es más fácil? Ni siquiera es más corto. Siempre he pensado que abusar de jQuery es una mala idea.
Manngo
2

Solo haz:

document.body.innerHTML = document.body.innerHTML + '<img src="../images/loaded.gif" alt="" onload="alert(\'test\');this.parentNode.removeChild(this);" />';
Lambder
fuente
1
parece una idea genial
pery mimon
0

Puede echar un vistazo a esta publicación . El código podría verse así:

var actualDivToBeUpdated = document.getElementById('test');
var div = document.createElement('div');
div.innerHTML = '<script type="text/javascript">alert("test");<\/script>';
var children = div.childNodes;
actualDivToBeUpdated.innerHTML = '';
for(var i = 0; i < children.length; i++) {
    actualDivToBeUpdated.appendChild(children[i]);
}
Darin Dimitrov
fuente
0

Gracias al script de Larry, que funcionó perfectamente bien en IE10, esto es lo que he usado:

$('#' + id)[0].innerHTML = result;
$('#' + id + " script").each(function() { this.text = this.text || $(this).text();} );
AxD
fuente
0

Extendiéndose de Larry's. Hice que buscara recursivamente todo el bloque y los nodos secundarios.
El script ahora también llamará a los scripts externos que se especifican con el parámetro src. Los guiones se adjuntan al encabezado en lugar de insertarse y se colocan en el orden en que se encuentran. Así que se conservan específicamente los scripts de orden. Y cada secuencia de comandos se ejecuta sincrónicamente de forma similar a cómo el navegador maneja la carga DOM inicial. Entonces, si tiene un bloque de script que llama a jQuery desde un CDN y el siguiente nodo de script usa jQuery ... ¡No hay problema! Ah, y etiqueté los scripts adjuntos con una identificación serializada basada en lo que estableciste en el parámetro de etiqueta para que puedas encontrar lo que agregó este script.

exec_body_scripts: function(body_el, tag) {
    // 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, id, callback) {
        var data = (elem.text || elem.textContent || elem.innerHTML || "" ),
            head = document.getElementsByTagName("head")[0] ||
                      document.documentElement;

        var script = document.createElement("script");
        script.type = "text/javascript";
        if (id != '') {
            script.setAttribute('id', id);
        }

        if (elem.src != '') {
            script.src = elem.src;
            head.appendChild(script);
            // Then bind the event to the callback function.
            // There are several events for cross browser compatibility.
            script.onreadystatechange = callback;
            script.onload = callback;
        } else {
            try {
                // doesn't work on ie...
                script.appendChild(document.createTextNode(data));      
            } catch(e) {
                // IE has funky script nodes
                script.text = data;
            }
            head.appendChild(script);
            callback();
        }
    };

    function walk_children(node) {
        var scripts = [],
          script,
          children_nodes = node.childNodes,
          child,
          i;

        if (children_nodes === undefined) return;

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

        return scripts;
    }

    var i = 0;
    function execute_script(i) {
        script = scripts[i];
        if (script.parentNode) {script.parentNode.removeChild(script);}
        evalScript(scripts[i], tag+"_"+i, function() {
            if (i < scripts.length-1) {
                execute_script(++i);
            }                
        });
    }

    // main section of function
    if (tag === undefined) tag = 'tmp';

    var scripts = walk_children(body_el);

    execute_script(i);
}
BadOPCode
fuente
0

Prueba esto, me funciona en Chrome, Safari y Firefox:

var script = document.createElement('script');
script.innerHTML = 'console.log("hi")';
document.body.appendChild(script); 
--> logs "hi"

Sin embargo, una cosa a tener en cuenta es que el siguiente script anidado en div NO se ejecutará:

var script = document.createElement('div');
script.innerHTML = '<script>console.log("hi")</script>';
document.body.appendChild(script);
--> doesn't log anything

Para que un script se ejecute, debe crearse como un nodo y luego agregarse como un hijo. Incluso puede agregar un script dentro de un div previamente inyectado y se ejecutará (me he encontrado con esto antes cuando intentaba que el código del servidor de anuncios funcione):

var div = document.createElement('div');
div.id = 'test-id';
document.body.appendChild(div);
var script = document.createElement('script');
script.innerHTML = 'console.log("hi")';
document.getElementById('test-id').appendChild(script);
--> logs "hi"
Trev14
fuente
0

Gastando la respuesta de Lambder

document.body.innerHTML = '<img src="../images/loaded.gif" alt="" > onload="alert(\'test\');this.parentNode.removeChild(this);" />';

Puede usar la imagen base64 para crear y cargar su script

<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMjHxIGmVAAAADUlEQVQYV2P4//8/AwAI/AL+iF8G4AAAAABJRU5ErkJggg=="
    onload="var script = document.createElement('script');  script.src = './yourCustomScript.js'; parentElement.append(script);" />

O si tienes un Iframepuedes usarlo en su lugar

<iframe src='//your-orginal-page.com' style='width:100%;height:100%'
    onload="var script = document.createElement('script');  script.src = './your-coustom-script.js'; parentElement.append(script);"
    frameborder='0'></iframe>
pery mimon
fuente
0

Necesitaba algo similar, pero necesitaba que el script permaneciera o se volviera a crear en el mismo lugar que el script original, ya que mi script apunta a la ubicación de la etiqueta del script en el DOM para crear / elementos de destino. También hice que el script fuera recursivo para asegurarme de que también funciona si está más de un nivel hacia abajo.

NOTA: Yo uso constaquí, si tiene un navegador más antiguo, solo use var.

    window.exec_body_scripts = function(body_el) {
        // ref: /programming/2592092/executing-script-elements-inserted-with-innerhtml based on Larry K's answer
        // 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.
        const
            type__Js = 'text/javascript',
            tagName__Script = 'script',
            tagName__Script__Upper = tagName__Script.toUpperCase();
        var scripts = [], script, i;
        function evalScript(elem) {
            var parent = elem.parentNode,
                data = (elem.text || elem.textContent || elem.innerHTML || ""),
                script = document.createElement(tagName__Script);

            script.type = type__Js;
            try {
                // doesn't work on ie...
                script.appendChild(document.createTextNode(data));
            } catch (e) {
                // IE has funky script nodes
                script.text = data;
            }
            // Make sure to re-insert the script at the same position
            // to make sure scripts that target their position
            // in the DOM function as expected.
            var parent = elem.parentNode;
            parent.insertBefore(script, elem);
            parent.removeChild(elem);
        };
        // Get all scripts (recursive)
        if (typeof (document.querySelectorAll) !== typeof (void 0)) {
            document.querySelectorAll('script').forEach((scr) => { if (!scr.type || scr.type.toLowerCase() === type__Js) scripts.push(scr); });
        }
        else {
            var children_nodes = body_el.childNodes, child;
            for (i = 0; children_nodes[i]; i++) {
                child = children_nodes[i];
                if (
                    child.nodeName
                    &&
                    child.nodeName.toUpperCase() === tagName__Script__Upper
                    &&
                    (
                        !child.type
                        ||
                        child.type.toLowerCase() === type__Js
                    )
                ) {
                    scripts.push(child);
                }
                // Recursive call
                window.exec_body_scripts(child);
            }
        }
        for (i = 0; scripts[i]; i++) {
            evalScript(scripts[i]);
        }
    };
NKCSS
fuente
0

Hizo esta nueva función auxiliar en TypeScript, tal vez alguien la aprecie. Si elimina la declaración de tipo del parámetro de secuencia de comandos, será simplemente JS.

const evalPageScripts = () => {
  const scripts = document.querySelectorAll('script');

  scripts.forEach((script: HTMLScriptElement) => {
    const newScript = document.createElement('script');
    newScript.type = 'text/javascript';
    newScript.src = script.src;

    if (script.parentNode) {
      script.parentNode.removeChild(script);
    }

    return document.body.appendChild(newScript);
  })
};

export default evalPageScripts;

kvba
fuente
-1

Prueba la función eval ().

data.newScript = '<script type="text/javascript">//my script...</script>'
var element = document.getElementById('elementToRefresh');
element.innerHTML = data.newScript;
eval(element.firstChild.innerHTML);

Este es un ejemplo real de un proyecto que estoy desarrollando. Gracias a esta publicación

IgniteCoders
fuente
-1

Aquí está mi solución en un proyecto reciente.

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Sample</title>
</head>
<body>
<h1 id="hello_world">Sample</h1>
<script type="text/javascript">
 var div = document.createElement("div");
  var t = document.createElement('template');
  t.innerHTML =  "Check Console tab for javascript output: Hello world!!!<br/><script type='text/javascript' >console.log('Hello world!!!');<\/script>";
  
  for (var i=0; i < t.content.childNodes.length; i++){
    var node = document.importNode(t.content.childNodes[i], true);
    div.appendChild(node);
  }
 document.body.appendChild(div);
</script>
 
</body>
</html>

gris
fuente