在客户端清理/重写HTML

时间:2008-11-17 13:34:33

标签: javascript html security html-sanitizing

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

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

是否有任何库至少可以

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

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

11 个答案:

答案 0 :(得分:99)

2016年更新:现在有一个基于Caja消毒剂的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注入攻击,因为<img src=bogus onerror=alert(1337)>将运行onerror处理程序,即使该节点从未附加到DOM。

答案 1 :(得分:38)

Google Caja HTML sanitizer可以通过将其嵌入web worker而成为“网络就绪”。清理程序引入的任何全局变量都将包含在worker中,并且处理将在其自己的线程中进行。

对于不支持Web Workers的浏览器,我们可以使用iframe作为清洁工的单独环境.Timothy Chien有一个polyfill就是这样做,使用iframe来模拟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(...)

工作者脚本只需要遵循这些说明:

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

答案 2 :(得分:16)

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

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

答案 3 :(得分:12)

你无法预料到某些浏览器可能会绊倒以避免黑名单的每种可能的奇怪类型的格式错误标记,所以不要列入黑名单。除了脚本/嵌入/对象和处理程序之外,您可能需要删除许多更多结构。

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

如果输入是格式良好的XHTML,则上述的第一部分要容易得多。

与HTML排版一样,如果您可以找到任何其他方法来避免这样做,请执行此操作。有许多潜在的漏洞。如果主要的网络邮件服务在这么多年后仍然发现漏洞,那么是什么让你认为你可以做得更好?

答案 4 :(得分:11)

既然所有主流浏览器都支持沙盒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;
};

您可以尝试here

请注意,我在此示例中禁止使用样式属性和标记。如果你允许,你可能想要解析CSS并确保它对你的目的是安全的。

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

答案 5 :(得分:7)

所以,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);

答案 6 :(得分:5)

String.prototype.sanitizeHTML=function (white,black) {
   if (!white) white="b|i|p|br";//allowed tags
   if (!black) black="script|object|embed";//complete remove tags
   var e=new RegExp("(<("+black+")[^>]*>.*</\\2>|(?!<[/]?("+white+")(\\s[^<]*>|[/]>|>))<[^<>]*>|(?!<[^<>\\s]+)\\s[^</>]+(?=[/>]))", "gi");
   return this.replace(e,"");
}

- 黑名单 - &gt;完成删除标记和内容

- 白名单 - &gt;保留标签

其他标签已删除但标签内容已保留

- 删除了白名单标签的所有属性(其余的属性)

答案 7 :(得分:1)

上面建议的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

答案 8 :(得分:1)

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

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

用法:

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

P.S。由于它使用浏览器来解析和处理DOM,因此其工作速度比“纯JavaScript”解决方案快得多。如果您对“纯JS”解决方案感兴趣,请尝试https://github.com/punkave/sanitize-html(不附带)

答案 9 :(得分:1)

我想到了一种使用原生 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>

答案 10 :(得分:0)

我建议在您的生活中切割框架,从长远来看,这将使您的工作变得更加轻松。

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

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

虽然我已经使用了一段时间的树木行者,但以下版本没有经过测试,它们是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.');
 }
}