PHP htmlspecialchars不能防御前端innerHTML产生的XSS注入

人盡茶涼 提交于 2019-12-10 09:27:36
不要在JS里直接用innerHTML输出未经JS过滤的用户内容,
即使这些内容已经经过服务器端PHP的htmlspecialchars或者HTMLPurifier过滤.
比如下面的代码,页面将alert弹出字符串/xss/,因为JS会把变量中的Unicode字符\u003c和\u003e转换成<和>输出.

<?php
//<img src=1 onerror=alert(/xss/)>
$xss = '\u003cimg src=1 onerror=alert(/xss/)\u003e';
//<a href=javascript:alert(String.fromCharCode(88,83,83))>XSS</a>
$xss = '\u003ca href=javascript:alert(String.fromCharCode(88,83,83))\u003eXSS\u003c/a\u003e';
header('Content-Type: text/html;charset=utf-8');
?>
<div id="xss"></div>
<script src="jquery.js"></script>
<script>
// PHP的htmlspecialchars不能过滤\u003c和\u003e,经过JS赋值后,Unicode字符\u003c和\u003e被转换成<和>.
var xss = '<?php echo htmlspecialchars($xss, ENT_QUOTES, 'UTF-8'); ?>';
console.log(xss);
//$("#xss").html(xss); //能够执行alert(/xss/) document.getElementById("xss").innerHTML = xss;
//$("#xss").html(xss.replace(/</g, "&lt;").replace(/>/g, "&gt;")); //不能执行alert(/xss/)
//$("#xss").text(xss); //不能执行alert(/xss/) document.getElementById("xss").innerText = xss;
</script>
$(#xss).append(xss)跟$("#xss").html(xss)输出的都是HTML.

解决方法:
http://segmentfault.com/q/1010000004067521
毕竟很多时候要把AJAX加载的数据用innerHTML添加到页面.
值得注意的是,innerHTML本质也是输出HTML,
所以我们可以在输出前用JS像PHP的htmlspecialchars那样
把特殊字符(&,",',<,>)替换为HTML实体(&amp;&quot;&#039;&lt;&gt;).
或者干脆直接用innerText(IE)和textContent(Firefox),也就是jQuery的text()来输出文本内容.
Firefox不支持IE的innerText,但支持textContent.
StackOverflow上找到的两个实现:
function htmlspecialchars(str) {
    return str
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#039;");
}
function htmlspecialchars(str) {
    var map = {
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#039;'
    };
    return str.replace(/[&<>"']/g, function(k) { return map[k]; });
}
其中g表示全局(global)替换的意思,也就是把字符串中的所有匹配的内容都进行替换.

不过JS模仿PHP的htmlspecialchars是一刀切的方法,数据将丧失HTML特性.
请教下,对于前端AJAX(PJAX)过来后的HTML数据大家是怎么过滤XSS输出的呢?
经过PJAX测试后发现,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';
    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 } ?>
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!