Execução de elementos <script> inseridos com .innerHTML

111

Eu tenho um script que insere algum conteúdo em um elemento usando innerHTML.

O conteúdo pode ser, por exemplo:

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

O problema é que o código dentro da <script>tag não é executado. Pesquisei um pouco no Google, mas não havia soluções aparentes. Se eu inserisse o conteúdo usando jQuery, $(element).append(content);as partes do script seriam obtidas evalantes de serem injetadas no DOM.

Alguém tem um trecho de código que executa todos os <script>elementos? O código jQuery era um pouco complexo, então não consegui descobrir como era feito.

Editar :

Observando o código jQuery, consegui descobrir como o jQuery faz isso, o que resultou no seguinte 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
fonte
1
Por que você não pode simplesmente iterar os filhos do elemento e, para cada um que é um elemento de script, você apenas eval () o innerHtml desse filho? É assim que eu vi isso ser feito por um grande fornecedor de componentes, cada vez que eles concluem um retorno de chamada ajax que adiciona coisas ao DOM, eles fazem exatamente isso. Porém, lembre-se de que ele pode ser lento, especialmente no IE7.
Slugster
2
Andreas: Se eu adicionar uma função, por exemplo, function testFunction(){ alert('test'); }ao código inserido no innerHTML, e depois tentar chamá-la, diz que a função não está definida.
phidah,
1
Phidah incrível, funciona como charme, saúde
Marcin
3
Acho que é absolutamente importante entender que esse é o comportamento pretendido do navegador para evitar ataques de script entre sites. Se o texto que você definiu como innerHTML for fornecido por Bob, ele será executado no navegador de Alice, causando danos (pense em um fórum onde as pessoas podem escrever comentários adicionando tags de script a eles). Você pode ler mais sobre isso aqui: en.wikipedia.org/wiki/Cross-site_scripting . Fique salvo!
Xatian
1
Um HTML mudou muito desde 2010. Hoje em dia, talvez você queira olhar: stackoverflow.com/a/58862506/890357
marciowb

Respostas:

27

O script do OP não funciona no IE 7. Com a ajuda do SO, aqui está um script que faz:

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
fonte
4
Melhor usar o jQuery $(parent).html(code)- veja minha resposta abaixo.
iirekm
depois que o script é injetado no DOM, como devo removê-lo?
S4beR
1
O script não é recursivo, portanto, examinará apenas os filhos diretos. Isso funciona para mim:if (nodeName(child, "script" ) && (!child.type || child.type.toLowerCase() === "text/javascript")) { scripts.push(child); } else { exec_body_scripts(child); }
st4wik
2
Observe que o código acima não executa scripts que carregam via src. O script acima pode ser alterado para verificar elem.srce definir condicionalmente a srcpropriedade do elemento de script criado em vez de definir seu conteúdo de texto.
Ryan Morlok
Por que não usar o evaltruque global em vez de criar um <script>elemento e inseri-lo no <head>? Ambos executam o código JS sem expor o encerramento atual.
Finesse
31

@phidah ... Aqui está uma solução muito interessante para o seu problema: http://24ways.org/2005/have-your-dom-and-script-it-too

Então, seria assim:

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

user447963
fonte
2
e ainda melhor, não há necessidade de ter uma imagem com evento "onerror", bom para injeção rápida de XSS jvfconsulting.com/blog/47/… :)
baptx
11
Você pode usar <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" onload="alert('test');">se quiser evitar uma solicitação http inútil.
Savas Vedova
1
adoro ! (adicionado style="display:none;) para ocultar o ícone da imagem quebrada
Kris
Na verdade, <style>é melhor do que <img>, porque não faz uma solicitação de rede
bacia de
23

Este é um script mais curto e eficiente que também funciona para scripts com a srcpropriedade:

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: embora evalpossa causar uma vulnerabilidade de segurança se não for usado corretamente, é muito mais rápido do que criar uma tag de script na hora.

DividedByZero
fonte
1
isso me ajudou, mas me sinto sujo usando eval. certificando-se de que o texto não pode ser comprometido, não vejo uma vulnerabilidade.
João
@ random-user evalfoi projetado para prejudicar os usuários. Qualquer execução de script dinâmico é um risco e é por isso que CSP o chama 'unsafe-eval'porque é. Você também está prejudicando a segurança de seus sites se estiver usando-o em uma biblioteca, pois eles não podem desligá-lo.
jonathanKingston
Testar isso no Chrome 44 causa um loop infinito quando appendChild é chamado, pois isso incrementa o valor scripts.length.
Codewithcheese
4
Scripts com a srcpropriedade serão baixados de forma assíncrona e executados quando chegados. A ordenação não é preservada. Os scripts embutidos também serão executados fora da ordem, de forma síncrona antes dos assíncronos.
robert4 de
21

Você não deve usar a propriedade innerHTML, mas sim o método appendChild do Nó: um nó em uma árvore de documento [HTML DOM]. Desta forma, você pode chamar mais tarde o código injetado.

Certifique-se de entender que node.innerHTML não é o mesmo que node.appendChild . Você pode querer passar algum tempo na Referência do cliente Javascript para obter mais detalhes e o DOM. Espero que o seguinte ajude ...

A injeção de amostra 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>

Saudações,

Andreas
fonte
2
Finalmente, alguém que realmente explica um pouco sobre o problema, em vez de todas as outras try this, look how clever I amrespostas. Merece um UV, pegou o meu.
RiggsFolly
eu uv isso porque é a maneira mais simples de injetar código javascript que é executado após a injeção. Eu apenas não entendo a diferença entre adicionar com innerHTML que não executa e a maneira acima com appendChild que executa. Usei isso com sucesso para criar uma página dinâmica com script do zero com socket.io
wetlip
2
var _in = document.getElementById(inject);, Eu acho que.
Ron Burk
21

Versão ES6 simplificada da resposta de @joshcomley com um exemplo.

Sem JQuery, sem biblioteca, sem eval, sem alteração de DOM, apenas 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
fonte
1
erro de digitação: o nome da função setInnerHtmlnão ésetInnerHTML
pery mimon
Observe que no plnkr que está vinculado, o nome da função é, setInnerHTMLmas está sendo chamado setInnerHtmlde dentro da runBfunção. Portanto, o exemplo não funciona
danbars de
16

Experimente este snippet:

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
fonte
sim, este método funciona, mas você obterá erros se tiver comentários ou console.logs, então esteja atento a isso também, você pode modificar para contabilizar os módulos var modules = [] var cleaner = text.replace (/ <script ([^> ] *)> ([\ s \ S] *?) <\ / script> / gi, função (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 no Chrome em meu projeto

Bruce
fonte
Rápido e bonito. Obrigado.
Jorge Fuentes González
É isso aí! Obrigado.
Floris
7

Uma solução sem 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);
  }
}

Essencialmente, isso clona a tag do script e, em seguida, substitui a tag do script bloqueada pela recém-gerada, permitindo a execução.

Joshcomley
fonte
3

scriptNode.innerHTML = codenão funcionou para o IE. A única coisa a fazer é substituir scriptNode.text = codee funcionar bem

Jorge
fonte
3

É mais fácil usar jquery em $(parent).html(code)vez 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
    })
}

Isso também funciona com scripts que usam document.writee scripts carregados via srcatributo. Infelizmente, mesmo isso não funciona com os scripts do Google AdSense.

iirekm
fonte
1
O que te faz dizer que é mais fácil? Não é nem mais curto. Eu sempre acho que o uso excessivo do jQuery é uma má ideia.
Manngo,
2

Apenas faça:

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

Você pode dar uma olhada neste post . O código pode ser assim:

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

Graças ao script de Larry, que funcionou perfeitamente bem no IE10, usei isto:

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

Estendendo fora da casa de Larry. Fiz uma busca recursiva em todo o bloco e nodos filhos.
O script agora também chamará scripts externos que são especificados com o parâmetro src. Os scripts são anexados ao cabeçalho em vez de inseridos e colocados na ordem em que são encontrados. Assim, especificamente, os scripts de pedidos são preservados. E cada script é executado de forma síncrona de forma semelhante à forma como o navegador lida com o carregamento inicial do DOM. Portanto, se você tem um bloco de script que chama jQuery de um CDN e o próximo nó de script usa jQuery ... Sem problemas! Ah, e eu marquei os scripts anexados com um id serializado baseado no que você definiu no parâmetro de tag para que você possa encontrar o que foi adicionado por 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
fonte
0

Experimente, funciona para mim no Chrome, Safari e Firefox:

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

Porém, uma coisa a ser observada é que o seguinte script aninhado em div NÃO será executado:

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

Para que um script seja executado, ele deve ser criado como um nó e depois anexado como um filho. Você pode até mesmo anexar um script dentro de um div injetado anteriormente e ele será executado (já encontrei isso antes ao tentar fazer o código do servidor de anúncios funcionar):

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

Gastando a resposta de Lambder

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

Você pode usar a imagem base64 para criar e carregar seu 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);" />

Ou se você tiver um, Iframevocê pode usá-lo em vez disso

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

Eu precisava de algo semelhante, mas precisava que o script permanecesse ou fosse recriado no mesmo local que o script original, uma vez que meu script visa a localização da tag do script no DOM para criar / direcionar os elementos. Também tornei o script recursivo para ter certeza de que também funcionará se estiver mais de um nível abaixo.

NOTA: Eu uso constaqui, se você tiver um navegador mais antigo, é só usar 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
fonte
0

Feito esta nova função auxiliar no TypeScript, talvez alguém goste. Se você remover a declaração de tipo do parâmetro de script, será apenas JS simples.

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

Experimente a função eval ().

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

Este é um exemplo real de um projeto que estou desenvolvendo. Graças a esta postagem

IgniteCoders
fonte
-1

Aqui está minha solução em um projeto recente.

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

cinzento
fonte