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

阅读 40.4k
10 个回答

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

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

嘟嘟嘟……

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

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

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

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

现在请听专家解读:

魔亦有道。

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

使用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这种事情交给前端,模板语言来做,对于前端,给她用什么就用什么,用的不爽自己适当的做变量调节就可以了。给她们用,让她们用的爽,用的简单,这是我们好男人的责任和义务,大家说对不对啊,嘻嘻。

你到底有没有测试过,就说你提到的那些场景能够绕过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%才是最凶猛的~~~),目前的统一做法是,做好输入检查,良好的编程意识,安全转义,借助第三方安全库。不要信任输入。也不要信任输出。

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

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

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏