【问题标题】:Sanitizing untrusted HTML5清理不受信任的 HTML5
【发布时间】:2013-07-17 05:26:14
【问题描述】:

我希望能够接受来自不受信任用户的 HTML 并对其进行清理,以便我可以安全地将其包含在我网站上的页面中。我的意思是,标记不应该被剥离或转义,而是应该基本不变地通过,除非它包含诸如<script><iframe>之类的危险标签,诸如onload之类的危险属性,或诸如背景之类的危险CSS属性网址。 (显然一些较旧的 IE 会在 CSS 中执行 javascript URL?)

提供来自不同域的内容并包含在 iframe 中,这不是一个好的选择,因为无法提前知道 iframe 必须有多高,因此对于某些页面来说总是看起来很丑。

我研究了 HTML Purifier,但它似乎还不支持 HTML5。我还研究了 Google Caja,但我正在寻找不使用脚本的解决方案。

有谁知道可以完成此任务的库吗? PHP是首选,但乞丐不能选择。

【问题讨论】:

  • 你可以试试raxan data sanitizer
  • 要求我们推荐或查找工具、库或最喜欢的场外资源的问题对于 Stack Overflow 来说是题外话,因为它们往往会吸引固执己见的答案和垃圾邮件。相反,请描述问题以及迄今为止为解决该问题所做的工作。

标签: html html-sanitizing


【解决方案1】:

黑名单方式让您面临升级压力。因此,每次浏览器开始支持新标准时,您都必须将您的清理工具提升到相同的水平。这种变化发生的频率比你想象的要多。

原因白名单(通过 strip_tags 实现明确定义的例外)会缩小用户的选项,但会将您置于保存站点。

在我自己的网站上,我的政策是在非常受信任的用户(例如管理员)的页面上应用黑名单,并在所有其他页面上应用白名单。这让我不会在黑名单上投入太多精力。借助更成熟的角色和权限概念,您甚至可以细化您的黑名单和白名单。


更新: 我猜你在找这个:

我明白 strip_tags 在标签级别列入白名单,但在属性级别接受所有内容。有趣的是,HTMLpurifier 似乎在属性级别进行了白名单。谢谢,在这里学习很愉快。

【讨论】:

  • strip_tags 无法防止危险属性。只要标签是允许的,它根本不接触属性。
【解决方案2】:

您可能可以按照以下方式做一些事情:

preg_replace('/<\s*iframe\s+[^>]*>.*<\s*\/\s*iframe\s+[^>]*>/i', '', $html);
preg_replace('/<\s*script\s+[^>]*>.*<\s*\/\s*script\s+[^>]*>/i', '', $html);
preg_replace('/\s+onload\s+=\s+"[^"]+"/i', '', $html);

...但话又说回来:你有 RegExes,现在你有两个问题 - 这可能会删除多余的东西,也会留下多余的东西。

但由于 HTML Purifier 可能是最现代和最适合(和开源)的项目,您仍然应该使用它,并且如果您真的需要它们,也许可以进行调整。

您也可以查看以下内容之一:

  • kses - 事实上的标准,也找到了进入 wordpress 的方法
  • htmLawed - 进一步开发的 kses
  • PHP Input Filter - 可以过滤标签和属性

尽管您还必须确保您自己的页面布局不会因为未关闭标签而影响包含结果。

【讨论】:

    【解决方案3】:

    也许采用不同的方法会更好? 告诉他们他们可以使用什么怎么样?

    在这种情况下,您可以使用 strip_tags。这样会更容易,更可控。将来也很容易扩展

    【讨论】:

    • > 此函数不会修改您允许使用 allowable_tags 的标签上的任何属性,包括恶作剧用户在发布将显示给其他用户的文本时可能滥用的样式和 onmouseover 属性。跨度>
    【解决方案4】:

    在 Ruby 上,我使用 Nokogiri (php version) 来解析 HTML 内容。您可以解析用户的数据并删除不必要的标签或属性,然后将其转换为文本。

    phpQuery - 另一个解析器。

    在 PHP 中有一个 strip_tags 函数。

    或者您可以手动删除所有属性:

    $dom = new DOMDocument;
    $dom -> loadHTML( $html );
    $xpath = new DOMXPath( $dom );
    $nodes = $xpath -> query( "//*[@style]" ); // all elements with style attribute
    foreach ( $nodes as $node ) {
        // remove or do what you want
        $node -> removeAttribute( "style" );
    }
    echo $dom -> saveHTML();
    

    【讨论】:

    【解决方案5】:

    WdHTMLParser 类。我将这个课程用于我的论坛。

    WdHTMLParser 示例:

    这个类将 HTML 解析为一个数组:

    <div>
        <span>
            <br />
            <span>
            un bout de texte
            </span>
            <input type="text" />
        </span>
    </div>
    

    数组:

    Array (
     [0] => Array (
      [name] => div
      [args] => Array ()
      [children] => Array (
       [0] => Array (
        [name] => span
        [args] => Array ()
        [children] => Array (
         [0] => Array (
          [name] => br
          [args] => Array ()
         )
         [1] => Array (
          [name] => span
          [args] => Array ()
          [children] => Array (
           [0] => un bout de texte
          )
         )
         [2] => Array (
          [name] => input
          [args] => Array (
           [type] => text
          )
         )
        )
       )
      )
     )
    )
    

    WdHTMLParser 数组转 HTML

    我在我的网站上使用这个类将数组转换为 HTML。

    • voyageWdHTML_allowattr :这些属性将被允许。

    • voyageWdHTML_allowtag :这些标签将被允许。

    • voyageWdHTML_special :制定自己的规则。实际上,我在每个链接中添加了“_blank”。并将&lt;br&gt;替换为pre标签中的换行符(\n)

    • fix_javascript :你可以开启/关闭这个功能,但是没用。

    示例 php:

    <?php
    include "WdHTMLParser.php";
    include "parser.php";
    
    list($erreur, $message) = (new Parser())->parseBadHTML("<div>
        <span>
            <a onclick=\"alert('Hacked ! :'(');\">Check javascript</a>
            <script>alert(\"lol\");</script>
        </span>
    </div>");
    
    if ($erreur) {
        die("Error : ".$message);
    }
    
    echo $message;
    

    输出:

    <div>
        <span>
            <a target="_blank">Check javascript</a>
            <pre>alert("lol");</pre>
        </span>
    </div>
    

    我的解析器类:

    <?php
    class Parser {
        //private function fix_javascript(&$message) { }
    
        private function voyageWdHTML_args($tab_args, $objname) {
            $html = "";
            foreach ($tab_args as $attr => $valeur) {
                if ($valeur !== null && $this->voyageWdHTML_allowattr($attr)) {
                    $html .= " $attr=\"".htmlentities($valeur)."\"";
                }
            }
            return $html;
        }
    
        private function voyageWdHTML_allowattr($attr) {
            return in_array($attr, array("align", "face", "size", "href", "title", "target", "src", "color", "style",
                                        "data-class", "data-format"));
        }
    
        private function voyageWdHTML_allowtag($name) {
            return in_array($name, array("br", "b", "i", "u", "strike", "sub", "sup", "div", "ol", "ul", "li", "font", "span", "code",
                                        "hr", "blockquote", "cite", "a", "img", "p", "pre", "h6", "h5", "h4", "h3", "h2", "h1"));
        }
    
        private function voyageWdHTML_special(&$obj) {
            if ($obj["name"] == "a") { $obj["args"]["target"] = "_blank"; }
            if ($obj["name"] == "pre") {
                array_filter($obj["children"], function (&$var) {
                    if (is_string($var)) { return true; }
                    if ($var["name"] == "br") { $var = "\n"; return true; }
                    return false;
                });
            }
        }
    
        private function voyageWdHTML($tableau, $lvl = 0) {
            $html = "";
            foreach ($tableau as $obj) {
                if (is_array($obj)) {
                    if (!$this->voyageWdHTML_allowtag($obj["name"])) {
                        $obj["name"] = "pre";
                        if (!isset($obj["children"])) {
                            $obj["children"] = array();
                        }
                    }
                    if (isset($obj["children"])) {
                        $this->voyageWdHTML_special($obj);
                        $html .= "<{$obj["name"]}{$this->voyageWdHTML_args($obj["args"], $obj["name"])}>{$this->voyageWdHTML($obj["children"], $lvl+1)}</{$obj["name"]}>";
                    } else {
                        $html .= "<{$obj["name"]}>";
                    }
                } else {
                    $html .= $obj;
                }
            }
            return $html;
        }
    
        public function parseBadHTML($message) {
            $WdHTMLParser = new WdHTMLParser();
            $message = str_replace(array("<br>", "<hr>"), array("<br/>", "<hr/>"), $message);
            $tableau = $WdHTMLParser->parse($message);
    
            if ($WdHTMLParser->malformed) {
                $retour = $WdHTMLParser->error;
            } else {
                $retour = $this->voyageWdHTML($tableau);
    
                //$this->fix_javascript($retour);// To make sur
            }
    
            return array($WdHTMLParser->malformed, $retour);
        }
    }
    

    WdHTMLParser 类

    <?php
    class WdHTMLParser {
        private $encoding;
        private $matches;
        private $escaped;
        private $opened = array();
        public $malformed;
        public function parse($html, $namespace = NULL, $encoding = 'utf-8') {
            $this->malformed = false;
            $this->encoding  = $encoding;
            $html            = $this->escapeSpecials($html);
            $this->matches   = preg_split('#<(/?)' . $namespace . '([^>]*)>#', $html, -1, PREG_SPLIT_DELIM_CAPTURE);
            $tree            = $this->buildTree();
            if ($this->escaped) {
                $tree = $this->unescapeSpecials($tree);
            }
            return $tree;
        }
        private function escapeSpecials($html) {
            $html = preg_replace_callback('#<\!--.+-->#sU', array($this, 'escapeSpecials_callback'), $html);
            $html = preg_replace_callback('#<\?.+\?>#sU', array($this, 'escapeSpecials_callback'), $html);
            return $html;
        }
        private function escapeSpecials_callback($m) {
            $this->escaped = true;
            $text          = $m[0];
            $text          = str_replace(array('<', '>'), array("\x01", "\x02"), $text);
            return $text;
        }
        private function unescapeSpecials($tree) {
            return is_array($tree) ? array_map(array($this, 'unescapeSpecials'), $tree) : str_replace(array("\x01", "\x02"), array('<', '>'), $tree);
        }
        private function buildTree() {
            $nodes = array();
            $i     = 0;
            $text  = NULL;
            while (($value = array_shift($this->matches)) !== NULL) {
                switch ($i++ % 3) {
                    case 0: {
                        if (trim($value)) {
                            $nodes[] = $value;
                        }
                    }
                        break;
                    case 1: {
                        $closing = ($value == '/');
                    }
                        break;
                    case 2: {
                        if (substr($value, -1, 1) == '/') {
                            $nodes[] = $this->parseMarkup(substr($value, 0, -1));
                        } else if ($closing) {
                            $open = array_pop($this->opened);
                            if ($value != $open) {
                                $this->error($value, $open);
                            }
                            return $nodes;
                        } else {
                            $node             = $this->parseMarkup($value);
                            $this->opened[]   = $node['name'];
                            $node['children'] = $this->buildTree($this->matches);
                            $nodes[]          = $node;
                        }
                    }
                }
            }
            return $nodes;
        }
        public function parseMarkup($markup) {
            preg_match('#^[^\s]+#', $markup, $matches);
            $name = $matches[0];
            preg_match_all('#\s+([^=]+)\s*=\s*"([^"]+)"#', $markup, $matches, PREG_SET_ORDER);
            $args = array();
            foreach ($matches as $m) {
                $args[$m[1]] = html_entity_decode($m[2], ENT_QUOTES, $this->encoding);
            }
            return array('name' => $name, 'args' => $args);
        }
        public function error($markup, $expected) {
            $this->malformed = true;
            printf('unexpected closing markup "%s", should be "%s"', $markup, $expected);
        }
    }
    

    要使用sur,可以使用这个功能(mybb.com):

    <?php
    class Parser {
        private function fix_javascript(&$message) {
            $js_array = array(
                "#(&\#(0*)106;?|&\#(0*)74;?|&\#x(0*)4a;?|&\#x(0*)6a;?|j)((&\#(0*)97;?|&\#(0*)65;?|a)(&\#(0*)118;?|&\#(0*)86;?|v)(&\#(0*)97;?|&\#(0*)65;?|a)(\s)?(&\#(0*)115;?|&\#(0*)83;?|s)(&\#(0*)99;?|&\#(0*)67;?|c)(&\#(0*)114;?|&\#(0*)82;?|r)(&\#(0*)105;?|&\#(0*)73;?|i)(&\#112;?|&\#(0*)80;?|p)(&\#(0*)116;?|&\#(0*)84;?|t)(&\#(0*)58;?|\:))#i",
                "#(o)(nmouseover\s?=)#i",
                "#(o)(nmouseout\s?=)#i",
                "#(o)(nmousedown\s?=)#i",
                "#(o)(nmousemove\s?=)#i",
                "#(o)(nmouseup\s?=)#i",
                "#(o)(nclick\s?=)#i",
                "#(o)(ndblclick\s?=)#i",
                "#(o)(nload\s?=)#i",
                "#(o)(nsubmit\s?=)#i",
                "#(o)(nblur\s?=)#i",
                "#(o)(nchange\s?=)#i",
                "#(o)(nfocus\s?=)#i",
                "#(o)(nselect\s?=)#i",
                "#(o)(nunload\s?=)#i",
                "#(o)(nkeypress\s?=)#i"
            );
            
            $message = preg_replace($js_array, "$1<b></b>$2$4", $message);
        }
    }
    

    【讨论】:

      【解决方案6】:

      我决定只使用 html5lib-python。这是我想出的:

      #!/usr/bin/env python
      import sys
      from xml.dom.minidom import Node
      import html5lib
      from html5lib import (HTMLParser, sanitizer, serializer, treebuilders,
                           treewalkers)
      
      parser = HTMLParser(tokenizer=sanitizer.HTMLSanitizer,
                          tree=treebuilders.getTreeBuilder("dom"))
      serializer = serializer.htmlserializer.HTMLSerializer(omit_optional_tags=False)
      
      document = parser.parse(sys.stdin.read(), encoding="utf-8")
      # find the <html> node
      for child in document.childNodes:
          if child.nodeType == Node.ELEMENT_NODE and child.nodeName == 'html':
              htmlNode = child 
      # find the <body> node
      for child in htmlNode.childNodes:
          if child.nodeType == Node.ELEMENT_NODE and child.nodeName == 'body':
              bodyNode = child
      # serialize all children of the <body> node
      for child in bodyNode.childNodes:
          stream = treewalkers.getTreeWalker("dom")(child)
          sys.stdout.write(serializer.render(stream, encoding="utf-8"))
      

      示例输入:

      <script>alert("hax")</script>
      <p onload="alert('this is a dangerous attribute')"><b>hello,</b> world</p>
      

      示例输出:

      &lt;script&gt;alert("hax")&lt;/script&gt;
      <p><b>hello,</b> world</p>
      

      【讨论】:

      • 编辑:这只适用于 Python 2。我也有一个适用于 Python 3 的版本,但我不会发布它,因为它有点 hackish。
      【解决方案7】:

      我个人将 HTML Purifier 用于此目的:

      http://htmlpurifier.org/docs

      它运行良好,允许您自定义每个标签和属性。到目前为止,我没有遇到过这个插件的安全问题。

      【讨论】:

      • HTML Purifier 还不支持 HTML5。
      • 但它允许你定义自己的标签和属性:)
      猜你喜欢
      • 2020-08-27
      • 1970-01-01
      • 1970-01-01
      • 2021-01-03
      • 2012-07-05
      • 2018-10-22
      • 2016-05-17
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多