PDO的深入理解

pdo可以通过预处理语句和参数绑定防注入,那么是不是就是绝对安全呢,根据之前的开发中踩的坑以及读到的一些相关资料,做一些总结。

 

本地预处理和模拟预处理

在初始化PDO驱动时,可以设置一项参数,PDO::ATTR_EMULATE_PREPARES,作用是打开模拟预处理(true)或者关闭(false)。手册描述

模拟预处理是防止某些数据库不支持预处理,比如sqllite,这样pdo会在本地进行预处理模拟。然后会对绑定的参数进行转义处理,最后将拼接好的sql语句发送给mysql服务器进行处理。也就是预处理工作并不是发生在数据库层面。与数据库的交互只有一次,而本地预处理只是将sql模板发送给数据库,数据库完成预处理操作,包括解析,编译,生成执行计划,之后pdo再将参数发给数据完成数据绑定,换句话说,为什么native prepare statment(数据库层面的prepare)可以防止sql注入,是因为sql模板已经交由数据库解析完成,sql语意确定,那么所有的参数都会被视作字符串参数,而不是sql语句或者表达式。

如果我们使用mysql,建议关闭模拟预处理。(某些低版本mysql不支持预处理除外)。

另外注意,即使mysql版本支持预处理,某些语句mysql无法支持prepare,那么pdo在处理时还是会使用模拟预处理。这样可能会存在注入的风险。

However, be aware that PDO will silently fallback to emulating statements that MySQL can’t prepare natively,参考这篇回答

 

字符集设置的正确性

还是上面那片问答,高票答主给了一个例子

$pdo->query('SET NAMES gbk');
$var = "\xbf\x27 OR 1=1 /*";
$query = 'SELECT * FROM test WHERE name = ? LIMIT 1';
$stmt = $pdo->prepare($query);
$stmt->execute(array($var));

为什么这个例子可以完成注入,答主也给了解释,原因就是上面所说的,pdo会进行模拟预处理,这样会在本地进行转义,具体转义可能类似于mysql_real_escape_string()(PHP 5.5.0 起已废弃,并在自 PHP 7.0.0 开始被移除)或者PDO::quote(),此类函数和addslashes这种函数区别在于,它会读取当前pdo驱动设置的字符集,而set names gbk并没有设置这个值,至于为什么,看鸟哥这篇文章,所以该函数在转义时还是认为pdo驱动的字符集是latin或者是utf8,于是它给x27转义,结果就变成了xbf5c27(x27对应ascii的\),该而数据库服务器的字符集被设置成gbk,于是xbf5c27被解释成

縗'

于是完成了注入,这也就是宽字节注入,经典的5c问题。

所以除了关闭模拟预处理之外,设置字符集也很重要,除了set names utf8,还需要设置pdo驱动的字符集,防止出现fallback成模拟预处理的情况,php5.6.3版本以上可以在初始化pdo时设置,如下

$pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=utf8', $user, $password);

或者mysqli_character_set_name函数 。

所以说最好的做法是关闭模拟预处理和设置正确的字符集。

 

另外补充几点:
1. MySQL客户端服务端针对预处理语句和非预处理语句定义了不同的数据传输协议。预处理语句使用所谓的二进制传输协议,MySQL服务端以二进制发送数据,发送之前不会被序列化。客户端不会仅接收字符串,相反的,他们将获取二进制数据并且尝试去转换值到合适的PHP数据类型。

2. 虽然预处理语句可以帮助我们防止sql注入,但是前提是我们必须正确的使用,例如下面代码就没用了:

$sql = 'select * from tab where name=' . $name . ' and password = ? limit 1';
$pdo->prepared($sql);
$pdo->execute([$password]);

这种需要保证所有的传入参数都先使用占位符代替。

 

参考:

https://www.v2ex.com/t/362646

https://github.com/wuzehv/blog/issues/32

http://zhangxugg-163-com.iteye.com/blog/1835721

服务容器-iocContainer

最近研究了一下服务容器的概念。网上也找了很多资料,也动手敲了不少代码,总结一下自己的看法。

服务容器应该不能说是一种技术,而是一种设计思想,主要解决了我们在代码中类依赖的问题。看个例子:

class Drive
{
    public $car;
    public function __construct(Car $car)
    {
         $this->car = $car;
    }

    public function do()
    {
         echo "drive ";
         $this->car->do();
    }
}
class People
{
    public $drive;
    public function __construct(Drive $drive)
    {
         $this->drive = $drive;
    }

    public function do()
    {
         echo "people ";
         $this->dirve->do();
    }
}
class Car { 
       public function do() { 
           echo "car";
       } 
} 

如代码所示,我们想要得到一个people类,必须依赖drive类,而drive类必须依赖car类,实际代码中可能比这个要复杂的多,如果有一种方法,我们直接传入people类名,就能返回people对象,多方便。这就是容器,容器可以帮助我们完成类依赖的自动处理和依赖注入等很多复杂的工作,对外则只需要很简单的调用。

这里有一个依赖注入的概念,对应的还有一个控制反转的概念,可以看看这篇博文介绍。

依赖注入

依赖注入和控制反转是对同一件事情的不同描述,它们描述的角度不同。依赖注入是从应用程序的角度在描述,应用程序依赖容
器创建并注入它所需要的外部资源。而控制反转是从容器的角度在描述,容器控制应用程序,由容器反向的向应用程序注入应用
程序所需要的外部资源。

那怎么实现一个容器呢,容器的核心就是帮我们理清一个类的依赖关系,比如上面的people类依赖drive类,那么我们怎么知道这种关系呢。其实php提供类很强大的反射机制,里面包括了很多对象,关于怎么使用,可以看这篇文章

反射和依赖注入

动手实现了一个简单的容器类,关于代码的解释可以看这篇文章

这里面有很多概念的东西,感觉写不出来,必须要多多敲它几百行代码方可破。

利用github-Webhooks自动部署代码

现在市面上大部分的仓库工具都提供webhooks功能,当触发某些特定的事件时,仓库会向指定地址发送一个http请求,利用此功能可以自动拉取代码完成部署。

当你在github上新建一个仓库,在setting里可以看到webhooks设置,我们设置好需要发送请求的地址以及触发事件,默认是push事件,不清楚的话,设置里有文档,如果不需要很复杂的功能,简单看一下就行了。

当然,请求会有个验签的动作(可选),建议选上,为了安全考虑。代码很简单,文档里有示例,php代码可以这么写:

$headers = getallheaders();
$payloadBody = file_get_contents("php://input");
$signature = $headers["X-Hub-Signature"];
$secretToken = getenv("SECRET_TOKEN");
$verifyHashHex = "sha1=" . hash_hmac("sha1", $payloadBody, $secretToken);
hash_equals($signature, $verifyHashHex);

验证成功后,执行:

popen("cd /path/to/rep && git pull --rebase", "r");

整个流程就结束了,在webhooks设置里能看到每次仓库发的请求。

下面写写我在完成整个过程中遇到的问题,总结一下,如果有朋友看到了这篇文章,能帮到你就再好不过。

  1. 调试代码,可以先写一个demo,利用exec函数,将命令执行结果输出,可以帮助我们排查问题
    echo getenv("HOME");exec("whoami", $o);
    print_r( $o );
    exec("git pull --rebase 2>&1", $o1);
    print_r( $o1 );

    如代码所示,先输出执行用户,再将错误信息也输出到标准输出。

  2. 用户主目录修改,如果你用的apache,并且没有该过配置文件的User指令,那么上面whoami命令输出的结果就是daemon用户,而该用户的主目录是/sbin,后面我们ssh公钥设置不方便,所以建议修改/etc/passwd,将主目录修改。可以在/home下新建daemon目录。
  3. 上面说到ssh设置,如果我们的remote是ssh方式访问,那么daemon主目录下新建下公钥吧,ssh-keygen,新建.ssh目录,另外一点要将该目录的权限设置成其他人不可访问,也就是说其他人不要给权限,否则后面拉取代码会出问题。
  4. .ssh下手动添加know_hosts文件,因为第一次拉取会有交互,必须你输入yes才会生成know_hosts文件,代码会卡住。
  5. HOME变量,如1,输出HOME变量,如果是/root,那么
    putenv("HOME=/home/daemon/");
  6. “error: unable to unlink old index.html(???)”,出现该错误还是权限问题,daemo用户对代码目录没有wx权限,也是就是不能删除文件。赋予即可。
  7. “insufficient permission for adding an object to repository database .git/objects”, objects目录及里面的文件可能没有权限写入。
    chmod -R g+w .git/objects

     

我在整个过程中遇到了很多问题,不过总算是完成了,记录一下。