【问题标题】:Sanitize/Rewrite HTML on the Client Side在客户端清理/重写 HTML
【发布时间】:2010-09-22 16:09:54
【问题描述】:

我需要显示通过跨域请求加载的外部资源,并确保只显示“安全”内容。

可以使用 Prototype 的 String#stripScripts 删除脚本块。但是onclickonerror 等处理程序仍然存在。

是否有任何图书馆至少可以

  • 剥离脚本块,
  • 杀死 DOM 处理程序,
  • 删除列入黑名单的标签(例如:embedobject)。

那么有任何与 JavaScript 相关的链接和示例吗?

【问题讨论】:

标签: javascript html security html-sanitizing


【解决方案1】:

现在所有主流浏览器都支持沙盒 iframe,我认为有一种更简单的方法可以保证安全。如果更熟悉此类安全问题的人可以查看此答案,我会很高兴。

注意:此方法在 IE 9 及更早版本中绝对行不通。有关支持沙盒的浏览器版本,请参阅 this table(注意:该表似乎说它在 Opera Mini 中不起作用,但我刚刚尝试过,它起作用了。)

这个想法是创建一个禁用 JavaScript 的隐藏 iframe,将不受信任的 HTML 粘贴到其中,然后让它解析它。然后你可以遍历 DOM 树,复制出被认为是安全的标签和属性。

此处显示的白名单只是示例。白名单的最佳选择取决于应用程序。如果您需要一个更复杂的策略,而不仅仅是标签和属性的白名单,则可以通过此方法实现,但此示例代码无法实现。

var tagWhitelist_ = {
  'A': true,
  'B': true,
  'BODY': true,
  'BR': true,
  'DIV': true,
  'EM': true,
  'HR': true,
  'I': true,
  'IMG': true,
  'P': true,
  'SPAN': true,
  'STRONG': true
};

var attributeWhitelist_ = {
  'href': true,
  'src': true
};

function sanitizeHtml(input) {
  var iframe = document.createElement('iframe');
  if (iframe['sandbox'] === undefined) {
    alert('Your browser does not support sandboxed iframes. Please upgrade to a modern browser.');
    return '';
  }
  iframe['sandbox'] = 'allow-same-origin';
  iframe.style.display = 'none';
  document.body.appendChild(iframe); // necessary so the iframe contains a document
  iframe.contentDocument.body.innerHTML = input;
  
  function makeSanitizedCopy(node) {
    if (node.nodeType == Node.TEXT_NODE) {
      var newNode = node.cloneNode(true);
    } else if (node.nodeType == Node.ELEMENT_NODE && tagWhitelist_[node.tagName]) {
      newNode = iframe.contentDocument.createElement(node.tagName);
      for (var i = 0; i < node.attributes.length; i++) {
        var attr = node.attributes[i];
        if (attributeWhitelist_[attr.name]) {
          newNode.setAttribute(attr.name, attr.value);
        }
      }
      for (i = 0; i < node.childNodes.length; i++) {
        var subCopy = makeSanitizedCopy(node.childNodes[i]);
        newNode.appendChild(subCopy, false);
      }
    } else {
      newNode = document.createDocumentFragment();
    }
    return newNode;
  };

  var resultElement = makeSanitizedCopy(iframe.contentDocument.body);
  document.body.removeChild(iframe);
  return resultElement.innerHTML;
};

安全漏洞:评论者@Explosion 指出href 属性可以包含JavaScript,例如&lt;a href="javascript:alert('Oops')"&gt;。应该可以在清理代码中捕获并删除它,但上述代码尚未(尚未)更新以执行此操作。

你可以试试here

请注意,我在此示例中不允许使用样式属性和标签。如果您允许它们,您可能希望解析 CSS 并确保它对您的目的是安全的。

我已经在几种现代浏览器(Chrome 40、Firefox 36 Beta、IE 11、Android 版 Chrome)和一个旧浏览器 (IE 8) 上对此进行了测试,以确保它在执行任何脚本之前被保释。我很想知道是否有任何浏览器遇到问题,或者我忽略了任何边缘情况。

【讨论】:

  • 这篇文章值得专家关注,因为它似乎是显而易见且最简单的解决方案。它真的安全吗?
  • 如何以编程方式创建隐藏的 iframe “禁用 JavaScript”?据我所知,这是不可能的。在你做iframe.contentDocument.body.innerHTML = input的那一刻,里面的任何脚本标签都会被执行。
  • @aldel 确实,我不知道。对我们来说,由于缺乏对 IE9 的支持,它仍然是不行的。我想您的解决方案可能有效,但我认为您应该在回复中澄清您依赖于 sandbox 属性。
  • @aldel - 抱歉,我的 VPN 由于某种原因阻止了脚本文件。
  • @pwray href 属性可以包含javascript:&lt;a href="javascript: alert('hello')"&gt;Test&lt;/a&gt;
【解决方案2】:

我没有使用正则表达式,而是想到了一种使用原生 DOM 的方法。通过这种方式,您可以将 HTML 解析为文档,获取该 HTML 并轻松获取所有特定元素以及要删除的白名单元素和属性。这使用属性列表作为允许的简单属性字符串数组,或者它可以使用正则表达式来验证它们的值并且只允许在某些标签上。

const sanitize = (html, tags = undefined, attributes = undefined) => {
    var attributes = attributes || [
      { attribute: "src", tags: "*", regex: /^(?:https|http|\/\/):/ },
      { attribute: "href", tags: "*", regex: /^(?!javascript:).+/ },
      { attribute: "width", tags: "*", regex: /^[0-9]+$/ },
      { attribute: "height", tags: "*", regex: /^[0-9]+$/ },
      { attribute: "id", tags: "*", regex: /^[a-zA-Z]+$/ },
      { attribute: "class", tags: "*", regex: /^[a-zA-Z ]+$/ },
      { attribute: "value", tags: ["INPUT", "TEXTAREA"], regex: /^.+$/ },
      { attribute: "checked", tags: ["INPUT"], regex: /^(?:true|false)+$/ },
      {
        attribute: "placeholder",
        tags: ["INPUT", "TEXTAREA"],
        regex: /^.+$/,
      },
      {
        attribute: "alt",
        tags: ["IMG", "AREA", "INPUT"],
        //"^" and "$" match beggining and end
        regex: /^[0-9a-zA-Z]+$/,
      },
      { attribute: "autofocus", tags: ["INPUT"], regex: /^(?:true|false)+$/ },
      { attribute: "for", tags: ["LABEL", "OUTPUT"], regex: /^[a-zA-Z0-9]+$/ },
    ]
    var tags = tags || [
      "I",
      "P",
      "B",
      "BODY",
      "HTML",
      "DEL",
      "INS",
      "STRONG",
      "SMALL",
      "A",
      "IMG",
      "CITE",
      "FIGCAPTION",
      "ASIDE",
      "ARTICLE",
      "SUMMARY",
      "DETAILS",
      "NAV",
      "TD",
      "TH",
      "TABLE",
      "THEAD",
      "TBODY",
      "NAV",
      "SPAN",
      "BR",
      "CODE",
      "PRE",
      "BLOCKQUOTE",
      "EM",
      "HR",
      "H1",
      "H2",
      "H3",
      "H4",
      "H5",
      "H6",
      "DIV",
      "MAIN",
      "HEADER",
      "FOOTER",
      "SELECT",
      "COL",
      "AREA",
      "ADDRESS",
      "ABBR",
      "BDI",
      "BDO",
    ]

    attributes = attributes.map((el) => {
      if (typeof el === "string") {
        return { attribute: el, tags: "*", regex: /^.+$/ }
      }
      let output = el
      if (!el.hasOwnProperty("tags")) {
        output.tags = "*"
      }
      if (!el.hasOwnProperty("regex")) {
        output.regex = /^.+$/
      }
      return output
    })
    var el = new DOMParser().parseFromString(html, "text/html")
    var elements = el.querySelectorAll("*")
    for (let i = 0; i < elements.length; i++) {
      const current = elements[i]
      let attr_list = get_attributes(current)
      for (let j = 0; j < attr_list.length; j++) {
        const attribute = attr_list[j]
        if (!attribute_matches(current, attribute)) {
          current.removeAttribute(attr_list[j])
        }
      }
      if (!tags.includes(current.tagName)) {
        current.remove()
      }
    }
    return el.documentElement.innerHTML
    function attribute_matches(element, attribute) {
      let output = attributes.filter((attr) => {
        let returnval =
          attr.attribute === attribute &&
          (attr.tags === "*" || attr.tags.includes(element.tagName)) &&
          attr.regex.test(element.getAttribute(attribute))
        return returnval
      })

      return output.length > 0
    }
    function get_attributes(element) {
      for (
        var i = 0, atts = element.attributes, n = atts.length, arr = [];
        i < n;
        i++
      ) {
        arr.push(atts[i].nodeName)
      }
      return arr
    }
  }
* {
  font-family: sans-serif;
}
textarea {
  width: 49%;
  height: 300px;
  padding: 10px;
  box-sizing: border-box;
  resize: none;
}
<h1>Sanitize HTML client side</h1>
<textarea id='input' placeholder="Unsanitized HTML">
&lt;!-- This removes both the src and onerror attributes because src is not a valid url. --&gt;
&lt;img src=&quot;error&quot; onerror=&quot;alert('XSS')&quot;&gt;
&lt;div id=&quot;something_harmless&quot; onload=&quot;alert('More XSS')&quot;&gt;
   &lt;b&gt;Bold text!&lt;/b&gt; and &lt;em&gt;Italic text!&lt;/em&gt;, some more text. &lt;del&gt;Deleted text!&lt;/del&gt;
&lt;/div&gt;
 &lt;script&gt;
    alert(&quot;This would be XSS&quot;);
  &lt;/script&gt;
</textarea>
<textarea id='output' placeholder="Sanitized HTML will appear here" readonly></textarea>
<script>
  document.querySelector("#input").onkeyup = () => {
    document.querySelector("#output").value = sanitize(document.querySelector("#input").value);
  }
</script>

【讨论】:

    【解决方案3】:

    [免责声明:我是作者之一]

    我们为此编写了一个“仅限网络”(即“需要浏览器”)开源库 https://github.com/jitbit/HtmlSanitizer,它删除了除“白名单”之外的所有 tags/attributes/styles

    用法:

    var input = HtmlSanitizer.SanitizeHtml("<script> Alert('xss!'); </scr"+"ipt>");
    

    附:比“纯 JavaScript”解决方案工作得快得多,因为它使用浏览器来解析和操作 DOM。如果您对“纯 JS”解决方案感兴趣,请尝试https://github.com/punkave/sanitize-html(非附属)

    【讨论】:

      【解决方案4】:

      2016 年更新:现在有一个基于 Caja sanitizer 的 Google Closure 软件包。

      它有一个更简洁的 API,经过重写以考虑现代浏览器上可用的 API,并与 Closure Compiler 更好地交互。


      无耻插件:请参阅 caja/plugin/html-sanitizer.js 了解经过彻底审查的客户端 html 清理程序。

      它是白名单,不是黑名单,但白名单可根据CajaWhitelists进行配置


      如果要删除所有标签,请执行以下操作:

      var tagBody = '(?:[^"\'>]|"[^"]*"|\'[^\']*\')*';
      
      var tagOrComment = new RegExp(
          '<(?:'
          // Comment body.
          + '!--(?:(?:-*[^->])*--+|-?)'
          // Special "raw text" elements whose content should be elided.
          + '|script\\b' + tagBody + '>[\\s\\S]*?</script\\s*'
          + '|style\\b' + tagBody + '>[\\s\\S]*?</style\\s*'
          // Regular name
          + '|/?[a-z]'
          + tagBody
          + ')>',
          'gi');
      function removeTags(html) {
        var oldHtml;
        do {
          oldHtml = html;
          html = html.replace(tagOrComment, '');
        } while (html !== oldHtml);
        return html.replace(/</g, '&lt;');
      }
      

      人们会告诉你,你可以创建一个元素,然后分配innerHTML,然后得到innerTexttextContent,然后转义其中的实体。不要那样做。它很容易受到 XSS 注入的影响,因为即使节点从未附加到 DOM,&lt;img src=bogus onerror=alert(1337)&gt; 也会运行 onerror 处理程序。

      【讨论】:

      • 很好,这里好像有一些文档:code.google.com/p/google-caja/wiki/JsHtmlSanitizer
      • Caja HTML sanitizer 代码看起来很棒,但需要一些胶水代码(邻近 cssparser.js,但更重要的是,html4 对象)。此外,它还污染了全局 window 属性。此代码是否有网页版?如果没有,您有没有比为它创建一个单独的项目更好的方法来生产和维护它?
      • @phihag,在google-caja-discuss 提问,他们可能会向您指出一个打包好的。我相信 window 对象污染是为了向后兼容,因此任何新的包版本都可能不需要它。
      • 原来已经有is a package 用于网络浏览器。
      • @phihag 这个包是给 nodejs 的,不是浏览器的。
      【解决方案5】:

      现在是 2016 年,我想我们中的许多人现在都在我们的代码中使用 npm 模块。 sanitize-html 似乎是 npm 上的主要选项,尽管有 others

      这个问题的其他答案为如何推出自己的问题提供了很好的输入,但这是一个非常棘手的问题,经过良好测试的社区解决方案可能是最好的答案。

      在命令行上运行它来安装: npm install --save sanitize-html

      ES5: var sanitizeHtml = require('sanitize-html'); // ... var sanitized = sanitizeHtml(htmlInput);

      ES6: import sanitizeHtml from 'sanitize-html'; // ... let sanitized = sanitizeHtml(htmlInput);

      【讨论】:

      • 这里是2018,这太重了(半兆依赖)
      • 2020 在这里,sanitize-html 适用于 Node,据我所知,浏览器仍然没有好的选择
      • 2021 在这里,看来 sanitize-html 的 v2 现在只有 ~80kB 并且可以在浏览器中使用。
      【解决方案6】:

      上面建议的 Google Caja 库过于复杂,无法配置并包含在我的 Web 应用程序项目中(因此,在浏览器上运行)。因为我们已经使用了 CKEditor 组件,所以我采取的做法是使用它内置的 HTML 清理和白名单功能,这更容易配置。因此,您可以在隐藏的 iframe 中加载 CKEditor 实例并执行以下操作:

      CKEDITOR.instances['myCKEInstance'].dataProcessor.toHtml(myHTMLstring)
      

      现在,当然,如果您没有在项目中使用 CKEditor,这可能有点矫枉过正,因为组件本身大约是半兆字节(最小化),但是如果您有源代码,也许您可​​以隔离将代码列入白名单 (CKEDITOR.htmlParser?) 并使其更短。

      http://docs.ckeditor.com/#!/api

      http://docs.ckeditor.com/#!/api/CKEDITOR.htmlDataProcessor

      【讨论】:

        【解决方案7】:

        可以通过将 Google Caja HTML sanitizer 嵌入到 web worker 中使其“支持网络”。 sanitizer 引入的任何全局变量都将包含在 worker 中,并且处理发生在它自己的线程中。

        对于不支持 Web Workers 的浏览器,我们可以使用 iframe 作为 sanitizer 工作的单独环境。Timothy Chien 有一个 polyfill 就是这样做的,使用 iframes 来模拟 Web Workers,所以这部分为我们完成了。

        Caja 项目在how to use Caja as a standalone client-side sanitizer 上有一个 wiki 页面:

        • 检查源代码,然后通过运行ant 构建
        • 在您的页面中包含 html-sanitizer-minified.jshtml-css-sanitizer-minified.js
        • 致电html_sanitize(...)

        worker 脚本只需要遵循这些说明:

        importScripts('html-css-sanitizer-minified.js'); // or 'html-sanitizer-minified.js'
        
        var urlTransformer, nameIdClassTransformer;
        
        // customize if you need to filter URLs and/or ids/names/classes
        urlTransformer = nameIdClassTransformer = function(s) { return s; };
        
        // when we receive some HTML
        self.onmessage = function(event) {
            // sanitize, then send the result back
            postMessage(html_sanitize(event.data, urlTransformer, nameIdClassTransformer));
        };
        

        (需要更多代码才能使 simworker 库正常工作,但这对本次讨论并不重要。)

        演示:https://dl.dropbox.com/u/291406/html-sanitize/demo.html

        【讨论】:

        • 很好的答案。 Jeffrey,你能解释一下为什么需要由网络工作者完成清理工作吗?
        • @AustinWang 网络工作者并不是绝对必要的,但由于清理可能在计算上可能很昂贵并且不需要用户交互,因此它非常适合该任务。 (我还提到在主要答案中包含全局变量。)
        • 我找不到适合这个库的文档。我在哪里/如何指定我的元素和属性白名单?
        • @AsGoodAsItGets 正如a comment in the current version 所描述的,nameIdClassTransformer 会为每个 HTML 名称、元素 ID 和类列表调用;返回null 将删除该属性。通过编辑src/com/google/caja/lang/html 中的 JSON 文件,您还可以自定义将哪些元素和属性列入白名单。
        • @JefferyTo 对不起,也许我太笨了,但我不明白。您引用的 JSON 文件未在上面的示例和演示中使用。我想在浏览器中使用该库,所以我查看了您的演示。你能修改上面的nameIdClassTranformer 函数吗?拒绝所有&lt;script&gt; 标签并接受&lt;b&gt;&lt;i&gt; 标签?
        【解决方案8】:

        我建议从你的生活中删除框架,从长远来看,它会让你的事情变得非常容易。

        cloneNode:克隆节点会复制其所有属性及其值,但不会复制事件侦听器

        https://developer.mozilla.org/en/DOM/Node.cloneNode

        虽然我已经使用 treewalkers 一段时间了,但以下内容没有经过测试,它们是 JavaScript 中最被低估的部分之一。下面是可以爬取的节点类型列表,一般我用SHOW_ELEMENT或者SHOW_TEXT

        http://www.w3.org/TR/DOM-Level-2-Traversal-Range/traversal.html#Traversal-NodeFilter

        function xhtml_cleaner(id)
        {
         var e = document.getElementById(id);
         var f = document.createDocumentFragment();
         f.appendChild(e.cloneNode(true));
        
         var walker = document.createTreeWalker(f,NodeFilter.SHOW_ELEMENT,null,false);
        
         while (walker.nextNode())
         {
          var c = walker.currentNode;
          if (c.hasAttribute('contentEditable')) {c.removeAttribute('contentEditable');}
          if (c.hasAttribute('style')) {c.removeAttribute('style');}
        
          if (c.nodeName.toLowerCase()=='script') {element_del(c);}
         }
        
         alert(new XMLSerializer().serializeToString(f));
         return f;
        }
        
        
        function element_del(element_id)
        {
         if (document.getElementById(element_id))
         {
          document.getElementById(element_id).parentNode.removeChild(document.getElementById(element_id));
         }
         else if (element_id)
         {
          element_id.parentNode.removeChild(element_id);
         }
         else
         {
          alert('Error: the object or element \'' + element_id + '\' was not found and therefore could not be deleted.');
         }
        }
        

        【讨论】:

        • 此代码假定要清理的输入已经被解析甚至插入到文档树中。如果是这种情况,则恶意脚本已经被执行。输入应该是一个字符串。
        • 然后发送一个 DOM 片段给它,仅仅因为它在 DOM 中以给定的形状或形式实际上并不意味着它已经被执行。假设他通过 AJAX 加载它,他可以将它与 importNode 结合使用。
        【解决方案9】:

        永远不要相信客户。如果您正在编写服务器应用程序,请假设客户端将始终提交不卫生的恶意数据。这是一个经验法则,可以让你远离麻烦。如果可以的话,我建议在服务器代码中进行所有验证和清理,您知道(在合理程度上)不会被摆弄。也许您可以使用服务器端 Web 应用程序作为客户端代码的代理,该代码从第 3 方获取并在将其发送到客户端本身之前进行清理?

        [编辑] 对不起,我误解了这个问题。不过,我坚持我的建议。如果您在将服务器发送给用户之前对其进行清理,您的用户可能会更安全。

        【讨论】:

        • 其实,随着node.js的火爆,javascript方案也可能是服务端方案。至少我是这样结束的。尽管如此,这是一个很好的建议。
        【解决方案10】:

        您无法预料到某个地方的某些浏览器可能会跳过所有可能的奇怪类型的畸形标记以逃避黑名单,因此不要将其列入黑名单。除了脚本/嵌入/对象和处理程序之外,您可能还需要删除许多更多的结构。

        而是尝试将 HTML 解析为层次结构中的元素和属性,然后根据尽可能少的白名单运行所有元素和属性名称。还要根据白名单检查您允许通过的任何 URL 属性(请记住,还有比 javascript 更危险的协议:)。

        如果输入是格式良好的 XHTML,那么上面的第一部分就容易多了。

        与 HTML 清理一样,如果您能找到任何其他方法来避免这样做,请改为这样做。有很多很多潜在的漏洞。如果主要的网络邮件服务在这么多年后仍然在寻找漏洞,那么是什么让您认为自己可以做得更好?

        【讨论】:

          猜你喜欢
          • 2014-07-13
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-02-20
          • 2020-09-07
          • 2020-06-18
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多