在客户端清理/重写HTML


80

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

可以使用Prototype的String#stripScripts删除脚本块。但是诸如onclick或的处理程序onerror仍然存在。

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

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

那么,那里有JavaScript相关的链接和示例吗?


12
不要相信答案可能通过正则表达式做到这一点stackoverflow.com/questions/1732348/...
米克Ohtamaa


这怎么安全?用户无法编辑页面的JavaScript吗?
丹尼尔(Daniel)说,恢复莫妮卡(Monica)2015年

是的,除非您只是试图防止可信任的用户犯错误,否则这不是“安全”的。
斯科特

Answers:


111

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注入的影响,因为即使节点从未连接到DOM,它也<img src=bogus onerror=alert(1337)>将运行onerror处理程序。


5
很好,看起来这里有一些文档:code.google.com/p/google-caja/wiki/JsHtmlSanitizer
tmcw 2011年

3
Caja HTML清理程序代码看起来不错,但需要一些粘合代码(cssparser.jshtml4对象相邻,但更重要的是,对象)。此外,它污染了全球window财产。是否有此代码的网络版本?如果没有,您是否看到一种比创建一个单独项目更好的方法来产生和维护它?
phihag,2012年

1
@phihag,请在google-caja-discuss上询问,他们可能会指出您是打包好的产品。我相信窗口对象污染是为了向后兼容,因此任何新的软件包版本可能都不需要这样做。
Mike Samuel

1
事实证明,已经有一个用于Web浏览器的软件包
phihag 2012年

2
@phihag该软件包用于nodejs,而不是浏览器。
杰弗里(Jeffery)在

40

可以通过将Google Caja HTML清理器嵌入到Web worker中来使其“支持网络” 。清理程序引入的所有全局变量都将包含在工作进程中,并且处理将在其自己的线程中进行。

对于不支持Web Workers的浏览器,我们可以将iframe用作单独的环境以供消毒程序使用。Timothy Chien有一个polyfill可以做到这一点,它使用iframes模拟Web Workers,因此我们可以完成一部分工作。

Caja项目有一个Wiki页面,介绍如何将Caja用作独立的客户端消毒剂

  • 签出源代码,然后通过运行进行构建 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


好答案。杰弗里(Jeffrey),您能解释一下为什么根本需要由网络工作者进行清理吗?
奥斯汀·王

@AustinWang Web工作者不是绝对必要的,但是由于清理工作可能在计算上很昂贵并且不需要用户交互,因此非常适合此任务。(我也提到在主要答案中包含全局变量。)
Jeffery To

我找不到该库的体面文档。我在哪里/如何指定元素和属性的白名单?
AsGoodAsItGets

@AsGoodAsItGets如当前版本中的注释所述nameIdClassTransformer每个HTML名称,元素ID和类列表都会被调用;返回null将删除该属性。通过在src / com / google / caja / lang / html中编辑JSON文件,您还可以自定义将哪些元素和属性列入白名单。
杰弗瑞(Jeffery)在

对不起,也许我太笨了,但我不明白。您引用的JSON文件未在上面的示例和演示中使用。我想在浏览器中使用该库,因此我看了您的演示。您可以修改nameIdClassTranformer上面的功能,例如拒绝所有<script>标签并接受<b><i>标签吗?
AsGoodAsItGet's

20

永远不要信任客户。如果要编写服务器应用程序,请假定客户端将始终提交不卫生的恶意数据。这是一条经验法则,可以使您摆脱麻烦。如果可以的话,我建议您在服务器代码中进行所有验证和清除操作,您知道(在一定程度上)不会被打扰。也许您可以使用服务器端Web应用程序作为客户端代码的代理,该客户端代码是从第三方获取并进行卫生处理之后再发送给客户端本身的?

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


19
实际上,随着node.js的普及,JavaScript解决方案也可能是服务器端解决方案。那就是我至少到这里结束的方式。尽管如此,这仍然是一个很好的建议。
Nicholas Flynt

15

现在,所有主要的浏览器都支持沙盒iframe,我认为可以采用一种更为简单的方法来确保安全。如果这个答案可以由更熟悉这种安全性问题的人来审查,我会很喜欢。

注意:此方法在IE 9及更早版本中肯定无法使用。请参阅此表的浏览器版本的支持沙盒。(注意:该表似乎说它在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;
};

您可以在这里尝试。

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

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


10
这篇文章值得专家关注,因为它似乎是最明显,最简单的解决方案。它真的安全吗?
pwray

如何以编程方式创建“禁用JavaScript”的隐藏iframe?据我所知这是不可能的。您执行的那一刻iframe.contentDocument.body.innerHTML = input,将执行其中的任何脚本标签。
AsGoodAsItGets

@AsGoodAsItGets-在iframe上查找沙箱属性。
aldel

1
@aldel确实,我对此一无所知。对我们来说,由于IE9中缺乏支持,它仍然是徒劳的。我想您的解决方案可能会起作用,但是我认为您应该在响应中阐明您依赖于该sandbox属性。
AsGoodAsItGet's

抱歉,我从开头“现在所有主要浏览器都支持沙盒iframe”中就清楚了。我将添加一个不太细微的注释。
aldel

12

您无法预期格式错误的标记的所有可能的怪异类型,某个地方的某些浏览器可能会跳出黑名单,因此请不要将其列入黑名单。有许多你可能需要的不仅仅是脚本/嵌入/对象和处理程序,除去更多的结构。

相反,尝试将HTML解析为层次结构中的元素和属性,然后针对尽可能少的白名单运行所有元素和属性名称。还要对照白名单检查您通过的所有URL属性(请记住,除了javascript:之外,还有更多危险的协议)。

如果输入格式正确的XHTML,则上面的第一部分会容易得多。

与HTML消毒一样,如果可以找到其他避免方法,请改为这样做。有很多潜在的漏洞。如果多年后主要的Webmail服务仍在发现漏洞,那么您认为自己可以做得更好吗?


11

所以是2016年,我想我们当中许多人npm现在在代码中使用模块。sanitize-html似乎npm上是领先的选择,尽管还有其他选择

该问题的其他答案为如何制定自己的建议提供了很好的信息,但这是一个棘手的问题,经过良好测试的社区解决方案可能是最佳答案。

在命令行上运行此命令以进行安装: 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);


10
2018在这里,这太重了(半兆的依赖项)
user1464581

2020年,sanitize-html适用于Node,据我所知,浏览器仍然没有很好的选择
Mick

2

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


2

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

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

用法:

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

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


0

我建议您淘汰框架,从长远来看,这会使事情变得非常容易。

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

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

尽管我已经使用树行者一段时间了,但以下内容尚未经过测试,它们是JavaScript最被低估的部分之一。这是您可以抓取的节点类型的列表,通常我使用SHOW_ELEMENTSHOW_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.');
 }
}

5
此代码假定要清理的输入已被解析,甚至已插入文档树中。在这种情况下,恶意脚本已经被执行。输入应为字符串。
phihag 2012年

然后向其发送一个DOM片段,只是因为它以给定的形状或形式存在于DOM中实际上并不意味着它已被执行。假设他是通过AJAX加载的,则可以将其与importNode结合使用。
约翰
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.