在添加到文档之前,使用JavaScript/jQuery创建DOM是否安全?

32

请仔细阅读以下声明: 假设在向 document 添加任何元素之前,$dom 中的所有不安全元素都已被移除。但它们最初已经被创建好了。好的,让我们继续...


如果处理用户文本并且可能会像这样加载:

var comment = 'I\'m a naughty person!!' +
              '<script src="http://blah.com/some_naughty_javascript.js">';
var $dom = $('<div>' + comment + '</div>');

这种情况本身有危险吗?我的意思是,仅仅创建DOM节点会不会注入任何内容,还是只是简单地处理并创建结构?

例如:

var $dom = $('<script>alert("hi");</script>');
显然,直到添加到document中,消息“hi”才会弹出。但是:
  • 通过这种方式创建的任何标签或其他内容是否可能存在危险性?
  • 在剥离坏元素并放置在文档中之前,Javascript / jQuery中的任何函数是否可以“监视”以此方式创建的元素并对其进行操作?

  • Bounty Edit

    因此,如下面的答案所概述的那样,这种方法似乎并不安全,特别是有一个原因:

    • var $dom = $('<img src="blah.jpg"/>')——这将立即请求图像,而不管对象是否已添加到文档中。

    这对于处理HTML ajax请求会带来很大的问题。例如,如果我们想要获取表单输入的值:

    $.ajax({
      url: 'test.php',
      success: function(responseHTML) {
        var inputs = $(responseHTML).find('form input');
      }
    });
    

    这将不自觉地导致浏览器请求所有图片。

    悬赏奖励:

    • 能够提供一个 nice、安全的方法来处理 ajax 请求,避免上述问题的任何人都会获得奖励。
    • 最好不要提供正则表达式答案... 比如说,如果我们想做 $(responseHTML).find('img') -- 不能用正则表达式删除图像标签,因此需要一种非侵入式的方式来阻止 src 加载,但仍然具有相同的属性、结构等。

    4
    你特别偏执,是不是?;) - Calum
    为什么如果你不想插入HTML,你还要解析它?如果你关心安全,为什么要接受用户的HTML? - gblazex
    @ galambalazs 看一下赏金编辑,这会产生各种影响,例如解析HTML ajax响应,即如果您只想插入选定的div或图像,则会无意中调用浏览器请求所有图像标记...这对性能不利。 “使用json”有时这不是实际或可选的选择。 - Gary Green
    3
    你正在向“你自己的”服务器请求HTML。为什么不在服务器端对HTML进行净化处理? - Raynos
    @Raynos,有时候这个并不是在我的服务器上使用的,而且我对页面没有控制权,例如一个Greasemonkey脚本。 - Gary Green
    显示剩余4条评论
    4个回答

    12

    这本身有危险吗?我的意思是,仅仅创建一个DOM节点是否会注入任何内容,还是只是简单地处理并创建结构?

    仅仅创建一个元素而不将其附加到DOM中不会导致任何脚本运行,因为此时它纯粹是一个对象(HtmlScriptElement)。当它实际附加到DOM中时,脚本元素将被浏览器评估和运行。话虽如此,我想一个极其狡猾的人可能会利用某些框架或浏览器中存在的漏洞来引发不良结果。

    考虑以下示例:

    <p>
        <input type="button" value="Store 'The Script' In Variable" id="store"/>
        <input type="button" value="Append 'The Script' To Dom" id="append"/>
    </p>
    <br/>
    <p>
        <input type="button" value="Does nothing"/>
    </p>
    <h1>The Script</h1>
    <pre id="script">
        $(function(){
            function clickIt(){
                $(this).clone().click(clickIt).appendTo("body");
            }
            $("input[type='button']").val("Now Does Something").click(clickIt);
        });
    </pre>
    
    var theScript;
    
    $("#store").click(function() {
        theScript = document.createElement('script');
        var scriptText = document.createTextNode($("#script").text());
        theScript.appendChild(scriptText);
    });
    
    $("#append").click(function() {
        var head = document.getElementsByTagName('head')[0];
        head.appendChild(theScript);
    });
    

    当您点击store时,它将创建HtmlScriptElement并将其存储到变量中。您会注意到即使对象已创建,也没有任何运行。一旦单击append,脚本将附加到dom并立即评估,按钮执行不同的操作。

    在jsfiddle上的代码示例


    有没有javascript/jquery的函数可以"监听"元素的创建,并在它被清除坏元素并放入文档之前对其进行操作?
    jQuery已经通过一些内部脚本评估为您完成了这项工作。
    引用自Karl Swedberg的文章关于.append()
    所有jQuery的插入方法都在内部使用domManip函数来清理/处理元素,在它们插入DOM之前和之后。 domManip函数之一是将要插入的任何脚本元素提取出来并通过"evalScript例程"运行它们,而不是将它们与其他DOM片段注入。它单独插入脚本,评估它们,然后从DOM中删除它们。
    您可以更改jQuery的行为,使其在调用append()时删除所有<script/>并消毒其他具有内联javascript onclick,mouseover等的元素,但这只会影响jQuery,因为某人可以轻松地使用vanilla javascript来附加<script/>元素。 Dom突变事件

    Dom Level 2定义了一些Dom变异事件来捕获添加到dom中的元素,可以查看事件DOMNodeInserted。但是它在元素已经被添加后才触发。需要注意的是,根据Raynos的说法,这些事件目前已弃用

    DOMNodeInserted 当节点作为另一个节点的子节点添加时触发。此事件在插入完成后分派。此事件的目标是正在插入的节点。冒泡:是 可取消:否 上下文信息:relatedNode保存父节点

    最终似乎没有完全阻止通过其他javascript将<script/>附加到dom的方法。(至少我找不到)。

    我能建议的最好方法是永远不要相信用户输入,因为所有用户输入都是邪恶的。当进行dom操作时,请仔细检查是否有禁止使用的标记,无论是<script/>还是普通的<p/>元素,并在持久化之前对所有输入进行消毒。

    正如John所指出的那样,您需要担心任何可以附加onclick事件或任何内联JavaScript事件处理程序的元素。


    感谢您的有趣观察。因此,如果在最终执行$dom.appendTo('body')时,所有脚本(以及任何其他“不良”元素)都被剥离,那么这似乎是一种相对安全的方法。似乎没有可靠的方法让用户“强制”DOM评估任何代码,因为正如您所说,“它们只是HTML元素的实例”。顺便说一句 - “仔细检查以确保没有无效标记” - 没有“无效”标记这样的东西;-)“<hello>”与“<ul>”一样有效。 - Gary Green
    @Gary,我调整了一下,说禁用标签或者你认为不安全并想去掉的标签。:) - Mark Coleman
    @Mark DOM变异事件目前已被弃用,规范正在重新编写。因此,请不要指望它们在不久的将来成为标准。 - Raynos
    3
    @Gary:不要忘记其他标签也可以包含JavaScript代码并且会被执行。例如,用户可以插入<a href="javascript:alert('breach your code'); return false;">恶意链接</a>。如果你允许它被插入到DOM中,那么他们就可以在另一个点上将自己的代码插入到你的页面中。 - John Fisher
    你们测试过像 <img src="" /> 这样的东西吗?我知道你可以在 ImageElement 上设置 .src 并且 URL 会被预加载。你可能可以通过这种方式造成一些损害。 - Raynos
    显示剩余6条评论

    8
    您的第一个示例的必要响应
    var comment = 'I\'m a naughty person!!' +
                  '<script src="http://blah.com/some_naughty_javascript.js">';
    var $dom = $('<div>' + comment + '</div>');
    

    不要这样做。相反,你应该使用一种将文本视为文本并且根本不会让你暴露于注入的API。在这个例子中,你应该这样做:
    var $dom = $('<div>').text(comment);
    

    这段代码创建了一个 div然后设置了其文本内容。由于 comment 没有被解析为 HTML,因此浏览器不会对其进行任何危险操作。

    接下来是问题

    如果我理解正确,您想从任意 HTML 中解析信息,而不让浏览器准备好显示它(比如加载图片)。

    这很棘手,因为内置于 Web 浏览器中的 DOM 是用来处理最终将要显示的内容的。jQuery(以及任何创建 DOM 节点的库)都受到此限制。

    DOM Level 2 定义了一个 API 来创建完全独立于活动文档的文档document.implementation.createHTMLDocument(title)。在我的测试中,当在其中一个这样的文档上创建 img 时,不会加载任何内容:

    var doc = document.implementation.createHTMLDocument(''),
        img = doc.createElement('img');
    img.src = 'http://example.com/image.jpg'; // Nothing happens.
    // Alternatively…
    doc.body.innerHTML = '<img src="http://example.com/image.jpg">'; // Nope.
    

    所以,用这种方式创建的文档似乎是一个很好的沙盒,可以解析和探索HTML。您甚至可以在不同文档中的节点周围创建一个jQuery包装器( $(doc.body)),并通过jQuery API进行探索。当您找到需要查找的节点时,可以将它们转换回HTML以插入活动文档中,或使用{{link1:像importNode()adoptNode()这样的方法}} 将它们直接传输到活动文档中。
    不幸的是,所有这些都是新支持的。Firefox从版本4开始支持createHTMLDocument(处理XML的类似createDocument方法在较旧版本中{{link2:可用}}),Internet Explorer在版本9及以上支持它{{link3:}}。此外,据我所知,规范不保证这些文档上不会预加载图像和脚本。
    更好的解决方案是避免浏览器的HTML解析器。最近出现了许多JavaScript HTML解析器,其中最简单的可能是John Resig的纯JavaScript HTML解析器。您可以向它提供HTML,并在其遇到新标签、属性和文本时触发回调。通过这些回调,您可以创建新的HTML,构建DOM节点或以任何形式存储文档 - 您可以忽略您认为危险的属性和节点。
    您可以在Dan Kaminsky的Interpolique中找到一个例子,这是一个旨在一劳永逸地消除XSS和SQL注入的概念验证。该项目尚未起步,但如果您下载Interpolique,您将在htmlparser.js底部找到一个safeParse()函数,它使用标签名称和属性的白名单,并丢弃其他所有内容。

    jsdom是一个完整的(符合DOM Level 2标准,部分符合Level 3)用JavaScript编写的HTML DOM,您可以使用它来安全地处理HTML。甚至可以加载其自己的jQuery副本。然而,它是为CommonJS编写的,没有考虑浏览器兼容性。我不知道它是否需要修改才能在大多数Web浏览器中运行。它还是一个庞大的库。

    如果可能的话,理想的解决方案是以HTML以外的格式提供AJAX响应。您是否需要包含额外的不安全HTML?如果您在服务器上只返回所需内容的工作...

    {
        "inputs": [
            '<input …>',
            '<input …>'
        ],
    }
    

    ...客户端的工作变得更加容易。


    有趣的是,createHTMLDocument 看起来很有前途,如果没有受限制的话。HTML解析器是不错的建议,尽管有点重,因为我认为一个简单的正则表达式就可以从图像中删除src,并将该数据推送到元素属性即 src2,以便仍然可用。我想我希望有一个神奇的属性可以停止这种预加载行为,看起来需要更脏的修复。此外,有时无法以另一种格式提供服务,例如用于Greasemonkey脚本。 - Gary Green

    2

    很好的问题。似乎可以注入脚本并在其中放置事件处理程序。我已经使用以下HTML进行了测试:

    <!DOCTYPE html>
    <html lang="en">
        <head>  
            <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
            <script type="text/javascript"> 
                <!-- 
                $(function() {
                    var $dom = $('<script>$(".testbutton").live("click", function() { alert("hi") });</script>');
    
                    $(".firstbutton").click(function() {
                        $("BODY").append($dom);
                    });
                });
                -->
            </script>
        </head>
    
        <body style="padding:0">            
            <button class="firstbutton">Click this first</button>
    
            <button class="testbutton">Then this</button>
        </body>
    </html>
    

    你可以看到,只有在第一个按钮被点击并将脚本标签添加到DOM后,第二个按钮才会起作用。
    如果用户表单输入被采取并动态插入到页面中,这可能会被滥用。在这种情况下,应该明确使用数据清理。
    这是我从未想过的事情 - 感谢你提出来。

    这并没有回答问题。问题是:如果您将元素插入页面,是否安全?您的答案是:如果您将元素插入页面,则不安全(但这不是问题所问的,而且这个事实已经在问题中说明)。 - D.W.
    @D.W. 确实如此,但这是对最初的问题的回答,在经过多次编辑和赏金添加之前。 ;) - Rory McCrossan

    0

    看起来只有在将脚本附加到DOM后,脚本才能正常工作。

    $(function ()
    {
        var ss = document.createElement('script');
        var scr = 'alert("bah");';
        var tt = document.createTextNode(scr);
        ss.appendChild(tt);
        var hh = document.getElementsByTagName('head')[0];
        //hh.appendChild(ss);
    });
    

    而且

    $(function ()
    {
        var ss = document.createElement('script');
        var scr = 'alert("bah");';
        var tt = document.createTextNode(scr);
        ss.appendChild(tt);
        var hh = document.getElementsByTagName('head')[0];
        hh.appendChild(ss);
    });
    

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接