前言
很多人喜欢直接在前端写ajax向后台指定地址提交数据,这样的做法过于草率,当时可能比较省事,后面付出的代价必定是惨痛的。笔者曾“有幸”经历过几次这样的跨域攻击,服务器遇到来自四面八方的海量请求,瞬间崩溃,日志显示请求来自不同国家和城市,这样一来常用的IP防火墙策略收效甚微。最终发现,服务器诸多接口安全性过低,省去了诸多校验,看似化繁为简,实则漏洞百出,攻击者在网上收集“肉鸡”,同时向指定服务器发送请求,致使服务器瘫痪。本文通过常用的dede(织梦)二次开发,
展示安全程度较高的前台数据提交策略,抛砖引玉,望读者日常开发过程勿避重就轻,忽视数据与信息安全。
环境与需求
- Centos 6.8
- LANMP
- DEDEV5.7
4 .需求:使用dede创建自定义表单,前台向后台自定义表单提交数据。 代码实现
切换到dede根目录 创建自定义标签解析文件 /include/taglib/csrftoken.lib.php
<?php
require_once(dirname(FILE).”/../helpers/cache.helper.php”);
if(!defined(‘DEDEINC’)){
exit(“Request Error!”);
}
/**
- 自定义csrf_token生成标签
*
- @version $Id: csrftoken.lib.php 1 10:11 2018年1月9日
- @package DedeCMS.Taglib
- @copyright Copyright (c) 2007 – 2010, DesDev, Inc.
- @author underclounds underclounds@gmail.com
- @license http://help.dedecms.com/usersguide/license.html
- @link http://www.dedecms.com
*/
/*>>dede>>
csrf签名标签
全局标记
V55,V56,V57
生成指定长度的随机加密字符串,每十分钟过期一次,用于前后台数据交互的验证 防止csrf攻击
{dede:csrftoken name=’token名 默认csrf’ len=’长度 默认12位’ exp=’过期时间 默认600秒’ /}
dede>>*/
function lib_csrftoken(&$ctag,&$refObj)
{
global $dsql,$envs;
//读取sessionId
@session_start();
$sessionId = session_id();
//属性处理
$attlist="name|csrf_token,len|12,exp|600";
//填充属性默认值
FillAttsDefault($ctag->CAttribute->Items,$attlist);
//读取标签属性
$name = $ctag -> GetAtt('name');
$len = (int) $ctag -> GetAtt('len');
$expires = (int) $ctag -> GetAtt('exp');
//读取缓存token
$cacheToken = GetCache($sessionId, $name);
if(empty($cacheToken)) {
//生成token
$token = randStr($len);
//将token存入缓存
$token = substr(sha1($token), 3, 32);
SetCache($sessionId, $name, $token, $expires);
} else {
$token = $cacheToken;
}
return $token;
}
/**
- 生成随机字符串
- @param $len int 长度
- @return string
*/
function randStr($len)
{
$str = ‘abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789’;
$token = ”;
for($i=0; $i<$len; $i++)
{
$token .= $str[rand(0,strlen($str)-1)];
}
return $token;
}
创建处理ajax请求文件 /plus/ajaxOrder.php
<?php
//header(“Access-Control-Allow-Origin:*”);//释放跨域
require_once(dirname(FILE).”/../include/common.inc.php”);
require_once(dirname(FILE).”/../include/helpers/cache.helper.php”);
global $dsql;
/**
- ———————————————
- Class ajaxOrder
- 处理ajax提交的用户预约信息
- @name ajax
- @package ajax
- @author underclounds underclounds@gmail.com
- / class ajaxOrder { /*
- @var string 客户姓名
*/
public $username;
/**
- @var string 预留手机
*/
public $tel;
/**
- @var string 预约分类
*/
public $sorts;
/**
- @var string 预留地址
*/
public $address;
/**
- @var int 面积
*/
public $area;
/**
- @var string 预约时间
*/
public $ordertime;
/**
- @var object $dsql 全局数据库操作对象(dede)
*/
private $dsql;
/**
- @var int 错误码
*/
protected $errcode = 1000;
/**
- @var string 提示信息
*/
protected $errmsg = ‘ok’;
/**
- 构造方法
- ajax constructor.
*/
public function __construct($dsql)
{
$this -> dsql = $dsql;
if(!empty($_POST[‘username’])) {
$this -> username = self::filter($_POST[‘username’]);
}
if(!empty($_POST[‘tel’])) {
$this -> tel = self::filter($_POST[‘tel’]);
}
if(!empty($_POST[‘sorts’])) {
$this -> sorts = self::filter($_POST[‘sorts’]);
}
if(!empty($_POST[‘address’])) {
$this -> address = self::filter($_POST[‘address’]);
}
if(!empty($_POST[‘area’])) {
$this -> area = self::filter($_POST[‘area’]);
}
if(!empty($_POST[‘ordertime’])) {
$this -> ordertime = self::filter($_POST[‘ordertime’]);
}
}
/**
- 检查所有参数
- @return bool
*/
public function check()
{
if(!$this->checkToken()) return false;
if(!$this->verifyName($this->username)) return false;
if(!$this->verifyTel($this->tel)) return false;
$this -> add();
return true;
}
/**
- 检查csrf_token是否正确
- @return bool
*/
private function checkToken()
{
@session_start();
$sessionId = session_id();
$token = $_SERVER[‘HTTP_X_CSRF_TOKEN’];
$cacheToken = GetCache($sessionId,’csrf_token’);
if(!empty($token) && !empty($cacheToken) && $token === $cacheToken) {
return true;
}
$this -> errcode = 1005;
$this -> errmsg = ‘非法请求’;
return false;
}
/**
- 写入预约数据
*/
private function add()
{
$sql = “INSERT INTO #@__diyform1
(id
, ifcheck
, username
, tel
, sorts
, address
, area
, ordertime
) “;
$sql .= “VALUES (NULL, 0, ‘$this->username’, ‘$this->tel’, ‘$this->sorts’, ‘$this->address’, ‘$this->area’, ‘$this->ordertime’); “;
$this -> dsql->ExecuteNoneQuery($sql);
}
/**
- 检验用户名
- @param $username string 用户名
- @return bool
*/
public function verifyName($username)
{
if(!empty($username)) {
return true;
}
$this -> errcode = 1001;
$this -> errmsg = ‘用户名不能为空’;
return false;
} /**
- 检验手机号码
- @param $tel
- @return bool
*/
public function verifyTel($tel)
{
if(!empty($tel)) {
if(!preg_match(‘/^1[345678]\d{9}$/’, $tel)) {
$this -> errcode = 1003;
$this -> errmsg = ‘手机号码格式不正确’;
return false;
}
if($this->telExist($tel)) {
$this -> errcode = 1004;
$this -> errmsg = ‘手机号码已预约’;
return false;
}
return true;
}
$this -> errcode = 1002;
$this -> errmsg = ‘手机号码不能为空’;
return false;
}
/**
- 检验手机是否存在
- @param $tel string 手机号
- @return bool
*/
public function telExist($tel)
{
$row = $this -> dsql ->GetOne(” SELECT id FROM #@__diyform1
WHERE tel = ‘{$tel}’ “);
return !empty($row[‘id’]);
}
//以下可实现类似方法…….. /**
- 一般过滤函数 防止注入
- @param $str string 字符串
- @return mixed
*/
public static function filter($str)
{
return preg_replace(‘/(delete|insert|select|update|drop|truncate|where)/i’,””, $str);
}
public function returnMsg(){
$errInfo = array(
‘errcode’ => $this -> errcode,
‘errmsg’ => $this -> errmsg
);
return json_encode($errInfo);
}
//魔术方法set
public function __set($name, $value)
{
$this -> $name = $value;
} //魔术方法get
public function __get($name)
{
return $this -> $name;
}
}
$ajax = new ajaxOrder($dsql);
$ajax -> check();
echo $ajax -> returnMsg();
前端测试代码
var token = "{dede:csrftoken exp='10'/}";
$.ajax({
url: 'http://localhost/plus/ajaxOrder.php',
type: 'post',
headers: {"X-CSRF-TOKEN" : token},
data: data,
dataType: 'json',
success:function (e) {
console.log(e);
}
});
失败返回示例如下:
成功返回示例如下:
注意:笔者使用缓存方式储存csrf_token 以SESSIONID作为识别用户标识,默认十分钟过期。
本文意在使用signature方式校验,故服务端除csrf_token外的校验写法较为简略,读者可自行扩展。