Выполнение элементов <script>, вставленных с помощью .innerHTML

112

У меня есть сценарий, который вставляет некоторый контент в элемент, используя innerHTML .

Например, контент может быть:

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

Проблема в том, что код внутри <script>тега не выполняется. Я немного погуглил, но очевидных решений не было. Если я вставлял контент с помощью jQuery, $(element).append(content);части скрипта получали evalдо того, как были введены в DOM.

У кого-нибудь есть фрагмент кода, который выполняет все <script> элементы? Код jQuery был немного сложным, поэтому я не мог понять, как это было сделано.

редактировать :

Заглянув в код jQuery, мне удалось выяснить, как это делает jQuery, в результате чего получился следующий код:

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>
фида
источник
1
Почему вы не можете просто перебирать дочерние элементы элемента, и для каждого из них, являющегося элементом сценария, вы просто eval () внутреннюю HTML этого дочернего элемента? Вот как я видел, как это делают крупные поставщики компонентов: каждый раз, когда они выполняют обратный вызов ajax, который добавляет что-то в DOM, они делают именно это. Имейте в виду, что это может быть медленным, особенно в IE7.
slugster
2
Андреас: Если я добавляю функцию, например, function testFunction(){ alert('test'); }в код, вставленный в innerHTML, а затем пытаюсь ее вызвать, это говорит о том, что функция не определена.
phidah
1
Потрясающая фида, работает как шарм, ура
Марчин
3
Я думаю, что абсолютно важно понимать, что такое поведение обозревателя предназначено для предотвращения атак с использованием межсайтовых сценариев. Если текст, который вы установили как innerHTML, предоставляется Бобом, он будет выполняться в браузере Алисы, вызывая повреждение (представьте себе форум, где люди могут писать комментарии, добавляя к ним теги сценариев). Подробнее об этом можно прочитать здесь: en.wikipedia.org/wiki/Cross-site_scripting . Оставайся экономным!
Xatian
1
HTML сильно изменился с 2010 года. В наши дни, возможно, вам захочется посмотреть: stackoverflow.com/a/58862506/890357
marciowb

Ответы:

27

Сценарий OP не работает в IE 7. С помощью SO вот сценарий, который выполняет:

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]);
  }
};
Ларри К.
источник
4
Лучше используйте jQuery $(parent).html(code)- см. Мой ответ ниже.
iirekm
после того, как скрипт внедрен в DOM, как его удалить?
S4beR
1
Скрипт не рекурсивен, поэтому он будет смотреть только на прямых потомков. У меня это работает:if (nodeName(child, "script" ) && (!child.type || child.type.toLowerCase() === "text/javascript")) { scripts.push(child); } else { exec_body_scripts(child); }
st4wik
2
Обратите внимание, что приведенный выше код не выполняет сценарии, загружаемые через src. Приведенный выше сценарий можно изменить для проверки elem.srcи условной установки srcсвойства созданного элемента сценария вместо установки его текстового содержимого.
Райан Морлок
Почему бы не использовать глобальный evalтрюк вместо создания <script>элемента и вставки его в <head>? Оба они выполняют код JS, не открывая текущее закрытие.
Finesse
31

@phidah ... Вот очень интересное решение вашей проблемы: http://24ways.org/2005/have-your-dom-and-script-it-too

Вместо этого это будет выглядеть так:

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

пользователь447963
источник
2
и даже лучше, нет необходимости иметь изображение с событием onerror, удобно для быстрой инъекции XSS jvfconsulting.com/blog/47/… :)
baptx
11
Вы можете использовать, <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" onload="alert('test');">если хотите предотвратить бесполезный HTTP-запрос.
Савас Ведова
1
любить это ! (добавлено style="display:none;), чтобы скрыть значок сломанного изображения
Крис
Фактически, <style> лучше <img>, потому что он не делает запрос сети
таз
24

Вот более короткий и эффективный сценарий, который также работает для сценариев со srcсвойством:

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

Примечание: хотя evalпри неправильном использовании может возникнуть уязвимость в системе безопасности, это намного быстрее, чем создание тега сценария на лету.

DividedByZero
источник
1
это помогло мне, но я чувствую себя грязным, используя eval. Убедившись, что текст не может быть скомпрометирован, я не вижу уязвимости.
Джон
@ random-user evalбыл разработан, чтобы навредить пользователям. Выполнение любого динамического сценария представляет собой риск, и именно поэтому CSP называет это 'unsafe-eval'так. Вы также наносите ущерб безопасности своих сайтов, если используете его в библиотеке, поскольку они не могут его отключить.
jonathanKingston
Тестирование этого в Chrome 44 вызывает бесконечный цикл при вызове appendChild, поскольку это увеличивает значение scripts.length.
Codewithcheese
4
Сценарии со srcсвойством будут загружаться асинхронно и выполняться по мере поступления. Порядок не сохраняется. Встроенные скрипты также будут выполняться вне очереди, синхронно перед асинхронными.
robert4
21

Вы должны использовать не свойство innerHTML, а метод appendChild узла: узел в дереве документа [HTML DOM]. Таким образом, вы сможете позже вызвать свой внедренный код.

Убедитесь, что вы понимаете, что node.innerHTML это не то же самое, что node.appendChild . Вы можете потратить некоторое время на Javascript Client Reference для получения более подробной информации и DOM. Надеюсь, следующее поможет ...

Образцы инъекционных работ:

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

С уважением,

Андреас
источник
2
Наконец, кто-то, кто на самом деле немного объясняет проблему, а не все другие try this, look how clever I amответы. Заслуживает УФ-излучения, получил мое.
RiggsFolly 07
Уф, потому что это самый простой способ внедрения кода javascript, который выполняется после внедрения. Я только не понимаю разницы между добавлением с помощью innerHTML, который не выполняется, и способом, описанным выше, с помощью appendChild, который выполняется. Я успешно использовал это для создания динамической страницы со скриптом с нуля с помощью socket.io
wetlip
2
var _in = document.getElementById(inject);, Думаю.
Рон Бурк
21

Упрощенная версия ES6 ответа @joshcomley с примером.

Без JQuery, без библиотеки, без eval, без изменения DOM, только чистый 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);
  });
}

использование

$0.innerHTML = HTML;    // does *NOT* run <script> tags in HTML
setInnerHTML($0, HTML); // does run <script> tags in HTML
Allenhwkim
источник
1
опечатка: имя функции setInnerHtmlне setInnerHTML
указано
Обратите внимание, что в связанном plnkr имя функции - setInnerHTMLно она вызывается setInnerHtmlизнутри runBфункции. Следовательно, пример не работает
данбарс 01
16

Попробуйте этот фрагмент:

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
источник
да, этот метод работает, но вы получите ошибки, если у вас есть комментарии или console.logs, так что будьте осторожны, вы также можете изменить учетную запись для модулей var modules = [] var cleaned = text.replace (/ <script ([^> ] *)> ([\ s \ S] *?) <\ / script> / gi, function (m, tags, script) {if (/type="module"/.test(tags)) {modules.push (скрипт) return} скрипты + = скрипт + '\ 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);  
   }  
}  

Он работает в Chrome в моем проекте

Брюс
источник
Быстро и красиво. Спасибо.
Хорхе Фуэнтес Гонсалес,
Это оно! Спасибо.
Флорис
7

Решение без использования 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);
  }
}

По сути, это клонирует тег сценария, а затем заменяет заблокированный тег сценария новым сгенерированным, что обеспечивает выполнение.

Джошкомли
источник
3

scriptNode.innerHTML = codeне работал для IE. Единственное, что нужно сделать, это заменить, scriptNode.text = codeи он отлично работает

Хорхе
источник
3

Легче использовать jquery $(parent).html(code)вместо 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
    })
}

Это также работает со скриптами, которые используют, document.writeи скриптами, загружаемыми через srcатрибут. К сожалению, даже это не работает со скриптами Google AdSense.

iirekm
источник
1
Что заставляет вас говорить, что это проще? Это даже не короче. Я всегда считаю, что чрезмерное использование jQuery - плохая идея.
Manngo
2

Просто сделать:

document.body.innerHTML = document.body.innerHTML + '<img src="../images/loaded.gif" alt="" onload="alert(\'test\');this.parentNode.removeChild(this);" />';
Lambder
источник
1
выглядят как гениальная идея
Pery Mimon
0

Вы можете взглянуть на этот пост . Код может выглядеть так:

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]);
}
Дарин Димитров
источник
0

Благодаря сценарию Ларри, который отлично работал в IE10, я использовал вот что:

$('#' + id)[0].innerHTML = result;
$('#' + id + " script").each(function() { this.text = this.text || $(this).text();} );
AxD
источник
0

От Ларри. Я сделал рекурсивный поиск по всему блоку и дочерним узлам.
Теперь сценарий также будет вызывать внешние сценарии, указанные с параметром src. Скрипты добавляются к заголовку, а не вставляются и размещаются в том порядке, в котором они были найдены. Так что специально сохраняются скрипты заказа. И каждый сценарий выполняется синхронно, подобно тому, как браузер обрабатывает начальную загрузку DOM. Итак, если у вас есть блок сценария, который вызывает jQuery из CDN, а следующий узел сценария использует jQuery ... Нет проблем! О, и я пометил добавленные сценарии сериализованным идентификатором, основанным на том, что вы установили в параметре тега, чтобы вы могли найти, что было добавлено этим сценарием.

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
источник
0

Попробуй, у меня это работает в Chrome, Safari и Firefox:

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

Следует отметить, что следующий сценарий, вложенный в div, НЕ запускается:

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

Для запуска скрипта он должен быть создан как узел, а затем добавлен как дочерний. Вы даже можете добавить скрипт внутри ранее введенного div, и он запустится (я уже сталкивался с этим раньше, пытаясь заставить работать код рекламного сервера):

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
источник
0

Расширяя ответ Ламбдера

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

Вы можете использовать образ base64 для создания и загрузки вашего скрипта

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

Или, если у вас есть, Iframeвы можете использовать его вместо

<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
источник
0

Мне нужно было что-то подобное, но мне нужно, чтобы сценарий оставался или воссоздавался в том же месте, что и исходный сценарий, поскольку мой сценарий нацелен на расположение тега сценария в DOM для создания / целевых элементов. Я также сделал сценарий рекурсивным, чтобы убедиться, что он также работает, если он находится более чем на один уровень ниже.

ПРИМЕЧАНИЕ. Я использую constздесь, если у вас более старый браузер, просто используйте 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
источник
0

Сделал эту новую вспомогательную функцию на TypeScript, может, кто-то это оценит. Если вы удалите объявление типа из параметра скрипта, это будет просто 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;

квба
источник
-1

Попробуйте функцию eval ().

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

Это реальный пример из проекта, который я разрабатываю. Благодаря этому сообщению

IgniteCoders
источник
-1

Вот мое решение в недавнем проекте.

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

Серый
источник