改良NoCSRF实现对PHP后端接口的安全验证

欢迎访问个人博客Aris-Blog

改良NoCSRF实现对PHP后端接口的安全验证

自己造的轮子,用于对前后端分离中后端接口的安全加固,如果有缺陷,还请指出,共同讨论改良!

改良和改造NoCSRF,实现对PhalAPI接口框架等前后端分离架构接口的安全加密认证。

不想看分析思路的可以直接跳到“实现过程”及上传的源码,参照进行部署。

目录:

  • NoCSRF的介绍
  • 配置到框架(以单次请求为示例)
  • 多次请求的处理
  • 解决方案
  • 实现过程
  • 结语

NoCSRF

国外大神开发的一个包,用于防范Web页面中的CSRF攻击。

代码一共有120行,思路很清晰,有兴趣可以进行拜读NoCSRF.php。

思路类似于常见的接口签名的实现:

  1. 请求头IP进行SHA1后,与20位随机码及时间戳连接,最后进行Base64处理。
  2. 每次请求接口前,生成上述$token存储到Session
  3. 携带$token请求接口。
  4. 后台验证时候逐步进行:
    • Session$token存在性检查
    • $_POST数组中$token存在性检查
    • 请求来源检查(请求头IP进行SHA1,与$token中的值进行对比)
    • 验证Session$_POST中的$token是否相同
    • 验证该$token是否过期(比对时间戳)
  5. 验证通过后,执行接口操作,否则抛出异常。
  6. 销毁$token

只要$token生成并存储的位置选择合理(每次页面加载前,PHP网页头部),基本不存在伪造的可能。因为$token生成时就放入了Session数组当中,存储在服务器硬盘Redis等缓冲区中,同时$token作为表单请求,后台将二者进行多重验证。

后面会介绍到,这个包只适用于一个页面对后端只有一次接口调用,多次请求需要进行改良。

配置到框架(以单次请求为示例)

配置前参见官网简单请求的示例:(PHP) NoCSRF

  1. 在框架命名空间中注册:
// nocsrf.php放入到/src/App/Common/
<?php
    namespace App\Common;
    ///。。。
    class NOCSRF{
    
    }
?>
  1. 页面头部生成Token:
<?php
    require_once("../vendor/autoload.php");//自动加载类
    use App\Common\NoCSRF;
    
    session_start();
    $token = NoCSRF::generate('csrf_token');
?>
  1. 表单携带Token:
<form name="csrf_form" action="#" method="post">
   <input type="hidden" name="csrf_token" value="<?php echo $token; ?>">
...Other form inputs...
    <input type="submit" value="Send form">
</form>

如果是Ajax,直接放入到变量当中,记得加上引号: var token = '<?php echo $token; ?>';

  • 后端验证:(对于每一个需要验证的接口,在构造函数内执行,./src/App/Api/xxx.php)
<?php

namespace App\Api;
use PhalApi\Api;

use App\Common\NoCSRF;
use PhalApi\Exception;

class Login extends Api{
    public function __construct(){
        session_start();

        try {
            // Run CSRF check, on POST data, in exception mode, with a validity of 10 minutes, in one-time mode.
            NoCSRF::check( $MainKey, $_POST, true, 60*10, false );
            // form parsing, DB inserts, etc.
        }
        catch ( Exception $e ) {
            exit('Need token!');
            // CSRF attack detected
        }
    } 

    public function getRules(){
    //.....
    }
    //...Other functions...
}
?>

以上内容针对:页面加载一次只请求后台一个接口的情形,比如登录。


多次请求的处理

对于一个页面同时请求多个接口,上述显然不适合。因为页面每次加载只会生成一个$token,而这个$token用于验证后,就会被后台销毁掉,同时请求的其他接口就会失效,而抛出Need Toekn!

思路1:(不可行)

Ajax请求接口的时候再<?php echo NoCSRF::generate('csrf_token');?>

比如下方例子,理论上可行,有请求就生成token,但实际上只有最后一次生成的token有效。因为PHP网页也算作脚本,页面每次刷新,页面内所有的PHP代码都会自动执行,所以前方的token在后方的token生成后被销毁。

function f1(){
    $.ajax({
            url: "xxxxx",
            type: "POST",
            data: {
                    'csrf_token': '<?php echo NoCSRF::generate('csrf_token');?>'
            },
            success: function(res, status, xhr) {
                    console.log(res);
            },
    })
}

function f2(){
    $.ajax({
            url: "xxxxx",
            type: "POST",
            data: {
                    'csrf_token': '<?php echo NoCSRF::generate('csrf_token');?>'
            },        
            success: function(res, status, xhr) {
                    console.log(res);
            },
    })
}
思路2:(可行但漏洞显而易见)

生成token单独作为接口发布,每次需要就先请求再获取。

于是有了以下方案:

  • 生成token的接口./src/App/Api/Token.php
<?php

namespace App\Api;

use PhalApi\Api;
use App\Common\NoCSRF;
use PhalApi\Exception;

class Token extends Api{
    public function getRules(){
        return array(
            'index' => array(),
        );
    }

    public function index(){
        session_start();
        return NoCSRF::generate('csrf_token');
    }
}
  • Ajax调取接口,封装成函数
function getCSRF(){
    let csrf_token = "";
    $.ajax({
            type: "GET",
            cache: false,
            async: false,
            url: "/xxx/xxx/?s=Token/Index",//Tokenj接口的URL
            success: function(res) {
                    csrf_token = res.data;
            }, error: function(XMLHttpRequest, textStatus, errorThrown) {
                    console.log(XMLHttpRequest.status);
                    console.log(XMLHttpRequest.readyState);
                    console.log(textStatus);
                    console.log(errorThrown);
                    csrf_token = "";
            }
    });
    return csrf_token;
}
  • 每次需要就执行:
function refresh(){
    $.ajax({
            url: "xxxxx",
            type: "POST",
            data: {
                    'csrf_token': getCSRF(),
                    'otherData' : 'xxx',
            },        
            success: function(res, status, xhr) {
                    console.log(res);
            },
    })
}

后面发现,直接用Postman请求/xxx/xxx/?s=Token/Index,获取到token,再携带这个token请求其他接口,依然能访问成功。

思考发现,这时的token仍然是在服务器端生成,无状态的HTTP请求直接拿过去,再反回来请求,依然是可行,只起到了验证时效性验证的功能,token与客户端没有唯一性联系,这种方案脱离了NoCSRF包本身的设计思路。


解决方案:

每次生成token的过程'NoCSRF::generate('csrf_token');',其中的'csrf_token'是自定义的,那么不妨把这个key利用起来,使之成为唯一且动态变化的值。

普通请求

在每个页面首部生成Token1,作为后面接口生成的tokentoken_key,请求是下面的样子:

改良后的请求

由于Token1是在页面首部(自身脚本,相当于与客户端绑定)生成的,不存在被伪造的可能 (原因见文章第一部分的介绍) ,故身份具有唯一性,拥有token的网页才可以访问接口。


实现过程:

注册接口: 每次页面加载会生成Token1,并请求此接口验证身份,当作token_key
token接口: 请求此接口会得到token_key: token2(上图)样式的Token用于业务接口的验证。
常规接口: 业务接口,比如“获取列表”。

  • 每个页面生成Token1并前往注册,注册成功Token1采纳,否则为空:(存储在session中)
<?php
    require_once("../vendor/autoload.php");
    use App\Common\NoCSRF;

    session_start();
    $token = NoCSRF::generate('csrf_token');
    $_SESSION['token_key'] = $token;//token_key或者Token1
?>

<html>
<body>
<script>
    var token_key = "";
    $.ajax({
            url: "/xxx/public/?s=Token/Login",//身份注册接口
            type: "POST",
            cache: false,
            async: false,
            data:{
                    "csrf_token": "<?php echo $token?>",
            },
            success: function(res) {
                    token_key = "<?php echo $token?>";
            }, error: function(error) {
                    console.log(error);
                    token_key = "";
            }
    });
</script>
</body>
</body>
  • 注册接口:./src/App/Api/Token.php(这里的key仍然是‘csrf_token’)
public function Login(){//页面头部的注册
    session_start();
    try {
        NoCSRF::check( 'csrf_token', $_POST, true, 60*10, false );
    }
    catch ( Exception $e ) {
        unset($_SESSION['token_key']);//验证不通过就销毁
        exit('Need token!');
    }
}
  • 拥有token_key后获取组合token: (此处开始,tokentoken_key作为键值)
        // ./src/App/Api/Token.php
    public function index(){
        session_start();
        $token_key = $_SESSION['token_key'];
        //generate函数的参数不再是'csrf_token'而是$token_key
        return NoCSRF::generate($token_key);
    }

    //Ajax请求Token,这一步无变化
    function getCSRF(){
            let csrf_token = "";
            $.ajax({
                    type: "GET",
                    cache: false,
                    async: false,
                    url: "/xxx/xxx/?s=Token/Index",             
                    success: function(res) {
                            csrf_token = res.data;
                    }, error: function(error) {
                            console.log(error);
                            csrf_token = "";
                    }
            });
            return csrf_token;
    }

    //请求业务接口,这里需要将Token1/token_key作为key,其中token_key就是页面首部生成,通过身份注册的
    <script>
        let json_data = {
                'data1' : 'xxx',
        };
        //注意,变量作为key传输必须用下方写法,不能用上面json格式写法,否则key直接为'token_key'.
        json_data[token_key] = getCSRF();

        $.ajax({
                url: "/xxx/xxx/?s=Order/GetList",
                type: "POST",
                data: json_data,        
                success: function(res, status, xhr) {
                        console.log(res);
                        //
                },
        })
     </script>
  • 业务接口验证(对于每一个需要验证的接口,在构造函数内执行,./src/App/Api/Order.php):
public function __construct(){
    session_start();

    //验证$_SESSION中是否存在'token_key'
    if(!isset($_SESSION['token_key'])){
        exit('Need token!');
    }
    $token_key = $_SESSION['token_key'];

    //注意下方check函数的第一个参数不再是'csrf_token'而是$token_key
    try {
        NoCSRF::check( $token_key, $_POST, true, 60*10, false );
    }
    catch ( Exception $e ) {
        exit('Need token!');
    }   
}

public function getRules(){
     //...
}

/// ...Other functions...

完结

至此,整个从前端请求和后端接口验证过程结束。至于如何部署到Phalapi框架或其他框架里面,相信看完整个过程就可以上手,也可以直接查看上传的示例。

本方案只针对采用前后端分离框架开发的微服务项目中,接口安全验证的防护。Web开发中涉及到方方面面的安全性问题:明文传输、数据库明文存储、XSS、渗透、社工等,要想让项目固若金汤,开发过程中都勇于去面对这些问题,寻找方案进行加固。

《鸟哥的Linux私房菜-服务器架设篇》和《大型网站技术架构》推荐阅读
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容

  • http://www.91ri.org/tag/fuzz-bug 通常情况下,有三种方法被广泛用来防御CSRF攻击...
    jdyzm阅读 4,172评论 0 5
  • Laravel 学习交流 QQ 群:375462817 本文档前言Laravel 文档写的很好,只是新手看起来会有...
    Leonzai阅读 7,887评论 2 12
  • 本博客转自:「作者:若愚链接:https://zhuanlan.zhihu.com/p/22361337来源:知乎...
    韩宝亿阅读 2,762评论 0 3
  • 曾经,我也以为自己是一个爱读书的人,但我错了--我不过是好读书而已!叶公好龙的“好”!我身上有“好读书”人的最基本...
    苏雅江南阅读 185评论 1 2
  • 我曾一度疯狂痴迷于彼岸花。“花开一千年,落一千年。花叶永不相见,情不为因果,缘注定生死。”传说,明明未曾谋面的曼珠...
    彦翕阅读 424评论 0 6