dedecms(织梦)数据提交防csrf请求校验

前言
很多人喜欢直接在前端写ajax向后台指定地址提交数据,这样的做法过于草率,当时可能比较省事,后面付出的代价必定是惨痛的。笔者曾“有幸”经历过几次这样的跨域攻击,服务器遇到来自四面八方的海量请求,瞬间崩溃,日志显示请求来自不同国家和城市,这样一来常用的IP防火墙策略收效甚微。最终发现,服务器诸多接口安全性过低,省去了诸多校验,看似化繁为简,实则漏洞百出,攻击者在网上收集“肉鸡”,同时向指定服务器发送请求,致使服务器瘫痪。本文通过常用的dede(织梦)二次开发,
展示安全程度较高的前台数据提交策略,抛砖引玉,望读者日常开发过程勿避重就轻,忽视数据与信息安全。
环境与需求

  1. Centos 6.8
  2. LANMP
  3. 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外的校验写法较为简略,读者可自行扩展。