PHP htmlspecialchars不能防護前端innerHTML產生的XSS注入

不要在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 } ?>
相關文章
相關標籤/搜索