PHP的防御XSS注入的终极解决方案【信息安全】【Hack】

Update20151202:
感谢大家的关注和回答,目前我从各种方式了解到的防御方法,整理如下:

  1. PHP直接输出html的,可以采用以下的方法进行过滤

    1.htmlspecialchars函数
    2.htmlentities函数
    3.HTMLPurifier.auto.php插件
    4.RemoveXss函数(百度可以查到)
  2. PHP输出到JS代码中,或者开发Json API的,则需要前端在JS中进行过滤

    1.尽量使用innerText(IE)和textContent(Firefox),也就是jQuery的text()来输出文本内容
    2.必须要用innerHTML等等函数,则需要做类似php的htmlspecialchars的过滤(参照@eechen的答案)
  3. 其它的通用的补充性防御手段

    1.在输出html时,加上Content Security Policy的Http Header
    (作用:可以防止页面被XSS攻击时,嵌入第三方的脚本文件等)
    (缺陷:IE或低版本的浏览器可能不支持)
    2.在设置Cookie时,加上HttpOnly参数
    (作用:可以防止页面被XSS攻击时,Cookie信息被盗取,可兼容至IE6)
    (缺陷:网站本身的JS代码也无法操作Cookie,而且作用有限,只能保证Cookie的安全)
    3.在开发API时,检验请求的Referer参数
    (作用:可以在一定程度上防止CSRF攻击)
    (缺陷:IE或低版本的浏览器中,Referer参数可以被伪造)
    

大概就是这些了,大家还有什么别的思路,欢迎补充!

——————————————————————————————————————————————————

原问题如下:

1.PHP如何完美(或者尽可能完美地)防御XSS攻击(比htmlspecialchars更完善的)?
2.我在想是不是防御XSS最好在前端做(毕竟JS在前端解析字符串都有坑啊)?
3.有木有什么解决方案或者思路啊,什么都行?

最近都在研究XSS防御的问题。

毕竟,比如用户注册的API,可能被Hacker利用,强行提交了"<script>alert('注入成功!')</script>"这样的用户名。

然后WEB前端怎么都要有显示用户名的地方吧。。。
于是。。。Boom。。。

直入重点:
我看到很多应对XSS的防御方案都是PHP的htmlentities函数或者htmlspecialchars。
随意百度了下,貌似ThinkPHP3.x默认就是用的htmlspecialchars。
比如:$str = htmlspecialchars($str, ENT_QUOTES);//替换掉<>&'"这5个字符
但是,只替换掉那几个字符真的够吗?

然后我发现了这个文章:
http://tieba.baidu.com/p/3003719171
使用\u003c\u003e在JS字符串中会被解释成<>的特性进行XSS攻击。。。
卧槽。。。

然后我想到了JS里的eval等等函数简直是无底洞。。。
然后我发现了这个文章:
http://www.2cto.com/Article/201310/251830.html
使用各种编码,各种手段执行JS,简直丧心病狂。
比如:<img src="x" onerror="&#97;&#108;&#101;&#114;&#116;&#40;&#49;&#41;">

啊!CAO。
我开始怀疑整个世界了。。。
所以,
我的问题是:

1.PHP如何完美(或者尽可能完美地)防御XSS攻击(比htmlspecialchars更完善的)?
2.我在想是不是防御XSS最好在前端做(毕竟JS在前端解析字符串都有坑啊)?
3.有木有什么解决方案或者思路啊,什么都行?

Update20151201:
能不要再复制粘贴答案,or迷信htmlspecialchars是无敌的了好嘛?
\u003cimg src=1 onerror=alert(/xss/)\u003e里的任何一个字符都是不会被htmlspecialchars处理的。
自己看图,对,就是你!

clipboard.png

阅读 28.1k
评论
    10 个回答
    xiasf
    • 1.9k

    这个问题我们还是先来请教一下砖家……

    现在马上为我们连线场外的砖家……

    嘟嘟嘟……

    砖家您好,请问这位同学的问题您怎么看?

    砖家:我趴在窗户上看……

    ……@#%&*!~~(@$%……

    好了,原来砖家是说最近雾霾严重,所以他只能趴在窗户上看这个问题……

    现在请听专家解读:

    魔亦有道。

    有专门的研究这些东西的,任何事只有专业领域的人做才会更有效率。

    使用HTMLPurifier才是终极理想。

    1. http://www.xcoder.cn/index.php/archives/971

    2. http://willko.iteye.com/blog/475493

    3. http://www.piaoyi.org/php/HTML-Purifier-PHP-xss.html

    4. http://www.edu.cn/ji_shu_ju_le_bu_1640/20080717/t20080717_310285.shtml

    5. http://www.111cn.net/phper/phpanqn/78018.htm

    6. http://security.ctocio.com.cn/securitycomment/54/8222554.shtml

    其实我还想说,我不希望防XSS这种事情交给前端,模板语言来做,对于前端,给她用什么就用什么,用的不爽自己适当的做变量调节就可以了。给她们用,让她们用的爽,用的简单,这是我们好男人的责任和义务,大家说对不对啊,嘻嘻。

    评论 赞赏
      eechen
      • 15.1k

      你到底有没有测试过,就说你提到的那些场景能够绕过htmlspecialchars呀,实践出真知.

      <?php
      $nowdoc = <<<'nowdoc'
      xss
      nowdoc;
      header('Content-Type: text/html;charset=utf-8');
      echo htmlspecialchars($nowdoc, ENT_QUOTES, 'UTF-8');
      

      补充:
      你说的对,毕竟很多时候要把AJAX加载的数据用innerHTML添加到页面.
      值得注意的是,innerHTML本质也是输出HTML,
      所以我们可以在输出前用JS像PHP的htmlspecialchars那样
      把特殊字符(&,",',<,>)替换为HTML实体(&amp;&quot;&#039;&lt;&gt;).
      或者干脆直接用innerText(IE)和textContent(Firefox),也就是jQuery的text()来输出文本内容.
      StackOverflow上找的两个实现:

      function escapeHtml(text) {
          return text
              .replace(/&/g, "&amp;")
              .replace(/</g, "&lt;")
              .replace(/>/g, "&gt;")
              .replace(/"/g, "&quot;")
              .replace(/'/g, "&#039;");
      }
      function escapeHtml(text) {
          var map = {
              '&': '&amp;',
              '<': '&lt;',
              '>': '&gt;',
              '"': '&quot;',
              "'": '&#039;'
          };
          return text.replace(/[&<>"']/g, function(m) { return map[m]; });
      }

      更正:
      经过测试后发现,AJAX返回的数据用jQuery的html()函数插入
      并不会因为Unicode字符u003c和u003e而发生XSS.

      index.php:
      <!DOCTYPE html>
      <html>
      <head>
      <meta charset="utf-8">
      <title>Index</title>
      <script src="jquery.js"></script>
      </head>
      <body>
      <div id="main">
          <a href="data.php">data.php</a>
          <script>
          $(document).ready(function() {
              $('#main').on('click','a',function(e) {
                  if(window.history.pushState) {
                      e.preventDefault(); //不跟随原链接跳转
                      url = $(this).attr('href');
                      $.ajax({
                          async: true,
                          type: 'GET',
                          url: 'data.php',
                          data: 'pjax=1',
                          success: function(data) {
                              window.history.pushState(null, null, url); //改变URL和添加返回历史
                              document.title = data.title; //设置标题
                              $('#main').html(data.main); //这里并不会因为Unicode字符\u003c和\u003e而发生XSS
                              //$('#main').html('\u003cimg src=1 onerror=alert(/xss/)\u003e'); //直接赋值字符串则会发生XSS
                          }
                      });
                  } else {
                      return; //低版本IE8等不支持HTML5 pushState,直接返回进行链接跳转
                  }
              });
          });
          </script>
      </div>
      </body>
      </html>
      
      data.php:
      <?php
      if(isset($_GET['pjax'])) {
          //PJAX请求返回JSON
          $arr['title'] = 'Data';
          $arr['main'] = '\u003cimg src=1 onerror=alert(/xss/)\u003e';
          //下面这两句是把PHP数组转成JSON对象返回
          header('Content-Type: application/json; charset=utf-8');
          echo json_encode($arr);
      } else {
          //常规请求返回HTML
      ?>
      <!DOCTYPE html>
      <html>
      <head>
      <meta charset="utf-8">
      <title>Data</title>
      <script src="jquery.js"></script>
      </head>
      <body>
      <div id="main">\u003cimg src=1 onerror=alert(/xss/)\u003e</div>
      </body>
      </html>
      <?php } ?>
      评论 赞赏

        首先,我想说,不要用你的无知来挑战大家

        这是道高一尺魔高一丈的东西

        html中的编码:

        < 进行编码
        html十进制: &#60;  
        html十六进制:&#x3c;
        url: %3C  
        base64: PA==

        javascript中的编码:

        <  进行编码
        八进制:\74  
        十六进制:\x3c 
        unicode:\u003c

        当然 htmlspecialchars 肯定是不行的,只能进行简单的处理,要不然还讨论什么xss了

        The translations performed are:
        
        '&' (ampersand) becomes '&amp;'
        '"' (double quote) becomes '&quot;' when ENT_NOQUOTES is not set.
        "'" (single quote) becomes '&#039;' (or &apos;) only when ENT_QUOTES is set.
        '<' (less than) becomes '&lt;'
        '>' (greater than) becomes '&gt;'

        上面代码还可以这样写

        <div id="a">test</div>
        <div id="b">test</div>
        <div id="c">test</div>
        <a href="javasc&NewLine;ript&colon;alert(/xss/)">click</a> 
        <a href="data:text/html;base64, PGltZyBzcmM9eCBvbmVycm9yPWFsZXJ0KDEpPg==">test</a>
        <script>
        var a="\u003cimg src=1 onerror=alert(/xss/)\u003e";
        var b="\74\151\155\147\40\163\162\143\75\170\40\157\156\145\162\162\157\162\75\141\154\145\162\164\50\61\51\76";
        var c="\u003c\u0069\u006d\u0067\u0020\u0073\u0072\u0063\u003d\u0031\u0020\u006f\u006e\u0065\u0072\u0072\u006f\u0072\u003d\u0061\u006c\u0065\u0072\u0074\u0028\u002f\u0078\u0073\u0073\u002f\u0029\u003e";
        document.getElementById("a").innerHTML=a;
        document.getElementById("b").innerHTML=a;
        document.getElementById("c").innerHTML=a;
        </script>

        但关键是,你确定你的那些代码可以提交吗?你要确定了再拿出来说

        比如最简单的href加入以下代码基本上歇菜了

        <base href="http://bbs.wdzj.com/" />
        评论 赞赏

          方法一,利用php htmlentities函数

          php防止XSS跨站脚本攻击的方法:是针对非法的HTML代码包括单双引号等,使用htmlspecialchars()函数 。

          在使用htmlspecialchars()函数的时候注意第二个参数, 直接用htmlspecialchars($string) 的话,第二个参数默认是ENT_COMPAT,函数默认只是转化双引号(“), 不对单引号(‘)做转义。

          所以,htmlspecialchars函数更多的时候要加上第二个参数, 应该这样用: htmlspecialchars($string,ENT_QUOTES).当然,如果需要不转化如何的引号,用htmlspecialchars($string,ENT_NOQUOTES)。

          另外, 尽量少用htmlentities, 在全部英文的时候htmlentities和htmlspecialchars没有区别,都可以达到目的.但是,中文情况下, htmlentities却会转化所有的html代码,连同里面的它无法识别的中文字符也给转化了。

          htmlentities和htmlspecialchars这两个函数对 '之类的字符串支持不好,都不能转化, 所以用htmlentities和htmlspecialchars转化的字符串只能防止XSS攻击,不能防止SQL注入攻击.

          所有有打印的语句如echo,print等 在打印前都要使用htmlentities() 进行过滤,这样可以防止Xss,注意中文要写出htmlentities($name,ENT_NOQUOTES,GB2312) 。

          方法二,给一个函数

          function xss_clean($data){
           // Fix &entity\n;
           $data=str_replace(array('&amp;','&lt;','&gt;'),array('&amp;amp;','&amp;lt;','&amp;gt;'),$data);
           $data=preg_replace('/(&#*\w+)[\x00-\x20]+;/u','$1;',$data);
           $data=preg_replace('/(&#x*[0-9A-F]+);*/iu','$1;',$data);
           $data=html_entity_decode($data,ENT_COMPAT,'UTF-8');
           // Remove any attribute starting with "on" or xmlns
           $data=preg_replace('#(<[^>]+?[\x00-\x20"\'])(?:on|xmlns)[^>]*+>#iu','$1>',$data);
           // Remove javascript: and vbscript: protocols
           $data=preg_replace('#([a-z]*)[\x00-\x20]*=[\x00-\x20]*([`\'"]*)[\x00-\x20]*j[\x00-\x20]*a[\x00-\x20]*v[\x00-\x20]*a[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iu','$1=$2nojavascript...',$data);
           $data=preg_replace('#([a-z]*)[\x00-\x20]*=([\'"]*)[\x00-\x20]*v[\x00-\x20]*b[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iu','$1=$2novbscript...',$data);
           $data=preg_replace('#([a-z]*)[\x00-\x20]*=([\'"]*)[\x00-\x20]*-moz-binding[\x00-\x20]*:#u','$1=$2nomozbinding...',$data);
           // Only works in IE: <span style="width: expression(alert('Ping!'));"></span>
           $data=preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?expression[\x00-\x20]*\([^>]*+>#i','$1>',$data);
           $data=preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?behaviour[\x00-\x20]*\([^>]*+>#i','$1>',$data);
           $data=preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:*[^>]*+>#iu','$1>',$data);
           // Remove namespaced elements (we do not need them)
           $data=preg_replace('#</*\w+:\w[^>]*+>#i','',$data);
           // http://www.111cn.net/
           do{// Remove really unwanted tags
            $old_data=$data;
            $data=preg_replace('#</*(?:applet|b(?:ase|gsound|link)|embed|frame(?:set)?|i(?:frame|layer)|l(?:ayer|ink)|meta|object|s(?:cript|tyle)|title|xml)[^>]*+>#i','',$data);
           }while($old_data!==$data);
           // we are done...
           return $data;
          }
          评论 赞赏

            永远不可能完美防御,但至少可以挡住99%(剩下的1%才是最凶猛的~~~),目前的统一做法是,做好输入检查,良好的编程意识,安全转义,借助第三方安全库。不要信任输入。也不要信任输出。

            评论 赞赏
              mcfog
              • 21.8k
              评论 赞赏

                Yii里面对xss做了很好的防范,如果需要,可以直接找Yii的过滤类,仿照浏览器对可执行代码的识别,如果遇到可执行代码,就过滤掉了

                评论 赞赏
                  kmxz
                  • 4.3k

                  为什么不用 CSP 直接一了百了呢?

                  评论 赞赏
                    评论 赞赏

                      有没有完美的分隔符把数据与指定完美区分起来

                      该答案已被忽略,原因:不符合答题规范 - 内容不是答案,可用评论、投票替代

                      评论 赞赏
                        撰写回答

                        登录后参与交流、获取后续更新提醒