CSRF 批量进行校验

CSRF是一个很普遍的问题,这里不对CSRF做过多的介绍,简单提一下,说一下自己在实际工作中的解决方案。

copy图(来自于网上):


核心问题

CSRF的核心问题在于:
1.目标网站不确定请求是否来自与指定的网站
2.目标网站不确定请求是否是用户的真实意愿,不安全网站是通过间接欺诈的手段在提交请求。

通用解决方案:

1.referer 校验。(问题:容易模仿)
2.token 校验。(问题:维护token麻烦)
3.签名认证。(问题:签名算法容易泄漏)

我们想要的效果:

1.我们需要对用户的每一个请求做校验来验证这是用户的本意操作。

2.我们需要大批量的验证不想维护token。

3.我们要预防CSRF。

我们的基本条件:

1.假设用户cookie不会泄露。

2.通过refer校验+签名认证(前端和后端维护同一个算法)+过期时间 的方法进行CSRF攻击防范。

3.我们的校验依赖于后台的名单,而不应该依赖于前台js的签名,所以我们需要在后台(服务端)维护一个校验名单。

具体思路:

1.js端通过cookie中的session id + 当天时间 按照一定的算法/规则做运算,在请求中将当前时间戳作为明文,运算出来的值作为秘文传给服务端。

2.服务端通过读取后台校验名单对符合规则的接口用同样的算法对session id +时间戳(前台传输)进行校验 并于结果进行对比,完成签名认证的过程。

3.在后台校验之前需要先进行refer校验。

4.每一个时间戳的有效期为2s,也就是从发起请求到收到请求不应该超过2s。(此步骤可以舍去或放开,未了防止重放攻击)


code:

对应的js算法

function getDefaultToken(type){
    var data = {};
    var obj = {};
    var str = '';
    var cookie = document.cookie;
    var arr=cookie.split(";"); 
    for(var  i=0;i<arr.length;i++){
        var arr2 = arr[i].split('=');
        obj[arr2[0]] = arr2[1];
        if( $.trim(arr2[0])=='PHPSESSID'){
            str +=  $.trim(arr2[1]);
            break;
        }
    }
    var time =  parseInt( (new Date).getTime(),10);
    str+=time;
    var len = str.length;
    var k=0,j=0,l=0;
    for(k;k<len;k++){
        l = str.charCodeAt(k);
        l = (l<<5) ;
        l = (l>0)?l:k;
        j +=  l;
    }
    j= j+time;
    if(!type){
        data['time_csrf'] = time;
        data['token_csrf'] = j;
         return  data;
    }
   return  time+'::'+j;
 }

对应的js请求方法:

function updateAdminPasswordSend(){
    var _data = {};
    _data.uid = user_id;
    _data.password = $("#newpassword").val();
    _data.comfirmpassword = $("#newpasswordcomfirm").val();
    var sign = $.getDefaultToken();
    _data.num = Math.random();
    //console.log(_data);
    if(_data.password != _data.comfirmpassword ){
        art.dialog({title:"修改密码",lock: true, content: "两次密码不一致",time:3});
        return;
    }
    if(_data.password == ''){
        art.dialog({title:"修改密码",lock: true, content: "密码不能为空",time:3});
        return;
    }
    $.post("{:U('Admin/updateAdminPassword')}",{"data":_data,"csrf_sign":sign},function(e){
        if(e.status == 1){
            $(".send_hide_none").removeClass("send_hide_none").addClass("send");
            art.dialog.get("show_auth").close();
        }else{
            art.dialog({title:"出错了",lock: true, content: e.info,time:3});
        }
    },'json');
    
}

对应的服务端校验名单(php 以 define作为校验名单)

<?php
/**
 * Created by PhpStorm.
 * User: wangyf
 * Date: 16-1-4
 * Time: 下午3:33
 */
/*
 * 校验名单命名规则:prefix_action_function(prefix 前缀用来标识所在系统如:管理后台Admin 也可以根据需求自己定义),对应常量值,表示的是传输过来后sign校验所在的下标
 * 白名单命名规则:white_prefix_action_function(white 为固定前缀,prefix,action,function同上),白名单后的值可以随意给,不做判断。
 * 每个系统可以自己维护自己的配置文件也可以维护在公共配置文件,建议各自维护各自的配置。白名单和黑名单只是为了更好的控制校验范围,具体问题具体分析
 * */
/*小二后台校验名单*/
//define('Admin_Admin_admin_list','csrf_sign');
define('Admin_Admin_add_admin_name','csrf_sign');
define('Admin_Admin_updateAdminPassword','csrf_sign');
define('Admin_Admin_delete_admin','csrf_sign');
define('Admin_Admin_ajax_upPhoneNum','csrf_sign');

/*小二后台校验白名单*/
//define('white_Admin_Admin_updateAdminPassword','csrf_sign');

对应的服务端构造函数中加入校验:

abstract class CommonAction extends Action
{

    private $admin_dict = array(
        '15833627275' => 0
    );

    /**
     * 基类初始化操作
     */
    protected function _initialize()
    {
        $this->CSRFCheck($_REQUEST['_URL_'][0],$_REQUEST['_URL_'][1],'Admin'); //csrf 校验
        $this->AdminAuth();
        $this->CheckAdminAuth();
    }
    //csrf 统一验证
    protected function CSRFCheck($class,$function,$prefix){
        $CSRFModel = new CSRFBascModel();
        $CSRFModel->crsfCheck($class,$function,$prefix);
    }
....

具体的CSRF校验类:

/**
 * Created by PhpStorm.
 * User: wangyf
 * Date: 16-1-4
 * Time: 上午11:39
 * bref:此类为CRSF验证类,主要功能是对crsf进行验证,此类目前是根据define配置来判断接口 并将验证收拢与此类中,方便后续迁移
 */
class CSRFBascModel extends BaseModel
{
    //默认的校验名称
    protected $signKey = 'time_csrf';
    protected $signVal = 'token_csrf';
    //crsf校验,包括refer和签名认证。
    public function crsfCheck($class,$function,$prefix){
        $key =$prefix.'_'.$class.'_'.$function;
        $whiteDefined =$this->definedCheck('white_'.$key);
        //验证名单,是为了兼容系统,后期可以去除,全部采用统一的传输方式,此处第一兼容白名单,第二兼容目前已有的ajax调用
        $defined = $this->definedCheck($key);
        $sign = array();
        //排除白名单
        if($whiteDefined) return true;
        //校验 校验名单
        if($defined){
            $define = constant($key);
            $sign = isset($_REQUEST[$define])?$_REQUEST[$define]:'';
        }else{
            //如果没有 校验 不再验证
            return true;
        }
        //校验refer
        $referOk = $this->referCheck();
        if(!$referOk) ApiAjaxReturnFail('refer error');
        //校验sign
        $signOk = $this->signCheck($sign,$key);
        if(!$signOk) ApiAjaxReturnFail('sign error');

    }
    //根据key返回是否有defined定义
    public function definedCheck($key){
        $whiteDefined = defined($key);
        return $whiteDefined;
    }
    //校验签名 对sign自动解析并校验
    public function signCheck($sign,$key){
        //所有的sign 数据统一按照类中的定义取出
        if(!is_array($sign)){
            $array = explode('::',$sign);
            $sign = array();
            if(isset($array[0]))
                $sign[$this->signKey] = $array[0];
            if(isset($array[1]))
                $sign[$this->signVal] = $array[1];
        }
        if(!isset($sign[$this->signKey]) || !isset($sign[$this->signVal])) return false;
        $sign_time = $sign[$this->signKey];
        $sign_token = $sign[$this->signVal];
        /*$time = microtime(true)*1000;
        $timeLimit = ($time-$sign_time);
        //超过2000毫秒 就是超时 主要为了防止get请求、重放攻击
        if($timeLimit>2000) {
            LogItil::write('sign check error: --request interface--'.$key.'--sign--'.json_encode($sign).'--check time--'.$time.'--time difference--'. $timeLimit .'--time--'. time().' this is error time out ---end',LogItil::LEVEL_FLOW);
            return false;
        }*/
        $cookie = $_COOKIE;
        $str =trim( $cookie['PHPSESSID']);
        $str.=$sign_time;
        $result = 0;
        $k = strlen($str);
        for($i=0;$i<$k;$i++){
            $r = $this->charCodeAt($str,$i);
            $r = $r<<5;
            $r = ($r>0)?$r:$i;
            $result +=$r;
        }
        $result = $result+$sign_time;

        if(strval($result) != strval($sign_token)){
            LogItil::write('sign check error: --request interface--'.$key.'--sign--'.json_encode($sign).'--PHPSESSID--'. $str .'--time--'. time().' this is error check error ---end',LogItil::LEVEL_FLOW);
           return false;
        }
        return true;
    }
    //校验refer 公共函数,但此处考录之后重构 以及测试环境 兼容 迁移在此处实现并收拢
    public function referCheck(){
        $referer = $_SERVER['HTTP_REFERER'];

        $matchUrl = "/^https?\:\/\/([a-zA-Z0-9\-]{0,16}\.){0,3}test\.com(:8080){0,1}(\/|$)/";
        # 线上环境才验证 长度为16 其余为32 为了兼容测试环境
        if(!isonline() ){
            $matchUrl = "/^https?\:\/\/([a-zA-Z0-9\-]{0,32}\.){0,3}test\.com(:8080){0,1}(\/|$)/";
        }
        if (!preg_match($matchUrl, $referer))
        {
            LogItil::write('refer check error: --refer--'. $referer .'--match--'.$matchUrl.'--time--'. time().' this is error refer prefix/port ---end',LogItil::LEVEL_FLOW);
            return false;
        }
        /**
         * 检查非法的后缀的refer
         */
        $illegal_refer = "/((.bmp)|(.png)|(.jpeg)|(.jpg))$/";
        if (preg_match($illegal_refer, $referer)) {
            LogItil::write('refer check error: --refer--'. $referer .'--match--'.$illegal_refer.'--time--'. time().' this is error refer postfix ---end',LogItil::LEVEL_FLOW);
            return false;
        }
        return true;
    }
     /*
     * charCodeAt 函数实现 ,获取某个字符串某位置的Unicode 编码 用于签名算法
     * @author wangyf
     * @time 20151213
     * */
    public function charCodeAt($str,$index)
    {
        $char = mb_substr($str, $index, 1, 'UTF-8');

        if (mb_check_encoding($char, 'UTF-8'))
        {
            $ret = mb_convert_encoding($char, 'UTF-32BE', 'UTF-8');
            return hexdec(bin2hex($ret));
        }
        else
        {
            return null;
        }
    }
}

具体需求可以根据具体情况,对其中的refer校验部分和对应的线上线下规则进行修改,2s超时可以适当放开或改进(也许不同的服务器时间会有出入,客户端的时间也会不同),整体思路就是以上部分 此处签名算法过于简单,后续可以根据自己的需要进行改进。

js可以封装在jquery扩展中自动提交,后台依赖于黑白名单校验,我们就可以通过配置黑白名单的方法进行快速大批量的页面CSRF校验。黑白名单只是便于控制校验范围,并不是必须的。

以上只是思路,和大家分享,共同进步。


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值