June 1, 2019 | 15:12

造轮子—NoCSRF实现对PHP后端接口的安全验证

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

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

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

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

流程图


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{
        
    }
    ?>
    
  2. 页面头部生成Token:

    <?php
    require_once("../vendor/autoload.php");//自动加载类
    use App\Common\NoCSRF;
        
    session_start();
    $token = NoCSRF::generate('csrf_token');
    ?>
    
  3. 表单携带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调取接口,封装成函数 javascript 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; }
  • 每次需要就执行: javascript 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 <?php require_once(“../vendor/autoload.php”); use App\Common\NoCSRF;

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


* 注册接口:`./src/App/Api/Token.php`(这里的key仍然是‘`csrf_token`’)
```php
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作为键值) ```php // ./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就是页面首部生成,通过身份注册的


* 业务接口验证(对于每一个需要验证的接口,在构造函数内执行,`./src/App/Api/Order.php`):
```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私房菜-服务器架设篇》和《大型网站技术架构》推荐阅读

© Aris 2020
鄂ICP备18010884号-1

Powered by Hugo & Kiss'Em.