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

使用Tideways监控php代码

最近研究了php性能调控相关工具,推荐一个tideways,还有一个xhgui界面化分析工具,帮助我们监控代码的运行。

关于怎么搭建,这里有一篇文章讲的很详细。

一些文章的补充:

1.  如果tideways安装失败,可以用以下方法安装:

wget https://github.com/tideways/php-xhprof-extension/archive/v4.1.5.tar.gz -O php-xhprof-extension-4.1.5.tar.gz
tar xzf php-xhprof-extension-4.1.5.tar.gz 
cd php-xhprof-extension-4.1.5 
phpize  
./configure --with-php-config=/php-config-path
make && make install 

2. 文章中给的是nginx 的配置,关于apache,php-value的写法如下(包含在httpd-vhost.conf或者.htaccess中):

<IfModule php7_module>
    php-value auto_prepend_file "/xhgui/path/to/external/header.php"
</IfModule>

3. install.php里会安装composer, 如果你已经安装了composer,可以手动执行 composer install 代替,如果失败,可以尝试执行 composer update

搭建好之后运行的效果如下:

用php创建一个守护进程

参考文章

示例代码

想到之前看到linux下,nohup和&一起使用可以创建一个后台job,例如:

nohup ping www.google.com & 1>/dev/null 2&>1

意思是让ping命令不接受hangup信号,也就脱离了终端的控制。该命令会在后台变成一个job:

[1]  33765

[1]表示job号码,33765代表进程id。使用jobs可以列出所有的jobs,也可以使用ps -ef|grep查看

bg %num命令可以将一个job后台执行

fg %num命令可以将一个命令前台执行  // num代表job号码

ctrl + z可以将一个job挂起暂停

kill %num 或 kill pid可以终止某个job

关于正则零宽断言的理解

最近项目里需要抓取别的网站的数据,于是重新回顾了一下正则,特别是零宽断言,下面是个人理解和总结。

基本概念说的很难理解,先上个例子:

<?php

preg_match_all("/(\w*)ing/", "aing bing", $mtches1);

preg_match_all("/(\w*)(?=ing)/", "aing bing", $mtches2);

print_r($matches1);
print_r($matches2);

刚开始我以为这两的结果是一样的,但其实不然,结果如下:

Array
(
    [0] => Array
        (
            [0] => aing
            [1] => bing
        )

    [1] => Array
        (
            [0] => a
            [1] => b
        )

)
Array
(
    [0] => Array
        (
            [0] => a
            [1] => 
            [2] => b
            [3] => 
        )

    [1] => Array
        (
            [0] => a
            [1] => 
            [2] => b
            [3] => 
        )

)

还是太天真。

于是从匹配过程上分析一波。

0a1b2i3n4g5 6b7i8n9g : 数字代表位置

第一个:先从位置0开始匹配,后的字符a匹配到\w,继续后面ing虽然能匹配到\w,但是它们是确定字符,所以匹配到第一个aing,于是继续从位置5开始,后面是空格,匹配不到\w,到位置6,b匹配到\w,继续向后,匹配到第二个bing,结束。于是分组1捕获的就是a和b。

第二个:同样从位置0开始,后的字符a匹配到\w,跳到位置1,此时控制权会交给后面的(?=ing),它断言位置1后面是ing字符,一看还真是,于是匹配到a,其实这里分不分组都一样,因为(?=exp)是零宽度,就是只占一个位置,继续从位置1开始往后匹配(因为(?=exp)是零宽),为什么空值成了第二个匹配到的值,我的理解是:因为*可以匹配空,也是零宽度,并且可以匹配到ing,类似于\0这种表示,也是就是说aing可能表示成a\0b\0i\0g\0这种。在往后就跟前面一样了,所以最后结果是a \0 b \0。

如果上面的\w*改成\w+,那么\0就不会被匹配到了。

看着很ok,但其实以上分析是不对的,如果把aing bing改成ainging bing,如果按照以上步骤,那么第二种情况应该是a \0 aing \0 b \0,但运行结果没有a和\0,????

其实先行断言的执行步骤是这样的先从要匹配的字符串中的最右端找到第一个ing(也就是先行断言中的表达式)然后 再匹配其前面的表达式,若无法匹配则继续查找第二个ing 再匹配第二个 ing前面的字符串,若能匹配 则匹配 ,意思是匹配是从最右边开始的,找到ing后,匹配b,找到第一个,再往前走,\0,再往前走aing,最后是\0。

负向零宽断言跟正向的两种情况是相反的。

果然正则是不适合地球人看的。

php7.2 不向后兼容的改动

做一个简单的总结,没有涵盖全部改动,因为不常见。

1. number_format()不会返回-0

因为-0这种写法很古怪,所以number_format(-0.01),会返回0。


2. 对数组强制转成对象时,会将整形key转成string

直接看代码,

$a = [0 => 1]; $a = (object)$a; print_r($a->{0}); //或者 print_r($a->{'0'});

现在允许这么写。但是key已经变成string类型,即’0’。


3. 不可以对不能count的对象调用count函数

没有实现Countable接口的对象都不能调用count函数,比如 count(1),count(‘a’) 在之前都会返回1,但是在7.2版本以后,会返回一个E_WARNING级别的错误,看一个例子:

class _count implements Countable{
    public function count() {
        return 10;
    }
}

$count = new _count();
echo count($count);  // 10

 


4. 可以对__PHP_Incomplete_Class类型的对象调用is_object方法

__PHP_Incomplete_Class,这个东西就是有一个对象,php没有找到定义它的地方,就会把该对象的类型变成这个,举个例子:

$serialized_object='O:1:"a":1:{s:5:"value";s:3:"100";}';

print_r( unserialize($serialized_object) );

// __PHP_Incomplete_Class Object ( [__PHP_Incomplete_Class_Name] => a [value] => 100 )

在解序列化的时候很常见,可以用unserialize_callback_func选项去避免,举个例子:

$serialized_object='O:1:"a":1:{s:5:"value";s:3:"100";}';

ini_set('unserialize_callback_func', 'mycallback'); // 设置回调

print_r( unserialize($serialized_object) );
function mycallback($classname) 
{
    require_once $classname.".php";
}

而在7,2版本对__PHP_Incomplete_Class对象调用is_object(),会返回true。


5. 使用一个未定义的常量,会产生一个E_WARNING级别的错误。

echo NAME;
// Warning: Use of undefined constant NAME

6. bcmod函数对于浮点数的操作

在之前,bcmod对于浮点数的运算类似于%操作符,但是在新版类似于fmod。

echo bcmod(4,3.5);  // 之前会返回1,现在会返回1,5

 

php:AES加密

常用的加密分为对称加密和非对称加密,对称加密就是发送方和接收方都用同一个秘钥进行加密解密,而非对称加密则使用一对公钥和私钥来进行加密,发送发只需要用接收方的公钥将数据加密即可。

AES是一种常见的对称加密算法,英语:Advanced Encryption Standard,又称Rijndael加密,它是一种分块加密方法,换句话说就是将明文块分成一组组小部分然后进行加密再组合,而根据分组大小可分为:AES-128,AES-196,AES-256三种,对应的分组大小分别是128bit,196bit,256bit。在网上转来的一张图:

而如何将这些明文块进行加密组合,又分为四种模式:ECB、CBC、CFB、OFB,除了第一种模式,后面三种模式都需要一个初始向量iv来辅助加密。初始向量的大小也是同区块大小保持一致,是个固定长度的随机字串,初始向量只是为了给加密算法提供一个可用的种子。

还有一点是,如果需要加密的源数据长度不是分组大小的整数倍,那么就需要对数据进行填充,一般的填充算法有PKSC5PKSC7填充算法。举个例子,如果源数据大小是10bytes,如果进行分组,还需要6bytes才能满足分组的要求。那么只需要在后面补上6个6即可:

x x x x x x x x x x 6 6 6 6 6 6

如果刚好不用填充,还需要再补齐区块大小个字节,这么做是为了在去除填充数据时,可以准确无误。

php中有一个mcrypt扩展,使用它提供的加密函数即可进行我们需要的对称加密。

mcrypt_encrypt(): 该方法接受5个参数,分别是算法名称,密钥,待加密源数据,加密模式,以及初始向量。最后一个参数,如果是ECB模式则不是必须的。需要注意的是如果密钥和初始向量的长度不是区块大小的长度,php会返回false。
mcrypt_decrypt(): 参数同加密方法一样,只不过源数据改成已加密后的数据。

php会使用 ‘\0 自动填充数据,你也可以使用自己的填充算法。

下面有一个简单的加密类,是参考网上的代码稍微修改的。使用的是PKSC7填充。

class Aes {
	private $init_key;

	private $auto_padding;

	private $aes_bytes;

	private $block_size;

	private $key;

	private $iv;

	private $map = [
		'128' => MCRYPT_RIJNDAEL_128,
		'196' => MCRYPT_RIJNDAEL_192,
		'256' => MCRYPT_RIJNDAEL_256,
	];

	public function __construct($init_key, $aes_bytes, $auto_padding){
		$this->init_key = $init_key;
		$this->auto_padding = $auto_padding;
		$this->aes_bytes = $aes_bytes;
		$this->block_size = mcrypt_get_block_size($this->map[$this->aes_bytes], MCRYPT_MODE_CBC);

		$this->key = substr( hash('sha256',$this->init_key), 0, $this->block_size );
		$this->iv  = mcrypt_create_iv( mcrypt_get_iv_size($this->map[$this->aes_bytes], MCRYPT_MODE_CBC) );
	}

	//CBC mode
	public function encrypt($data){
		$data = trim($data);
		if(!$this->auto_padding) {
			$data = $this->addPKCS7Padding($data);
		}
		$encrypt = mcrypt_encrypt($this->map[$this->aes_bytes], $this->key, $data, MCRYPT_MODE_CBC, $this->iv);
		return base64_encode( $encrypt );
	}

	public function decrypt($aes_data){
		$aes_data = base64_decode($aes_data);	
		$decrypt =  mcrypt_decrypt($this->map[$this->aes_bytes], $this->key, $aes_data, MCRYPT_MODE_CBC, $this->iv);
		return $this->stripPKCS7Padding( trim($decrypt) );
	}

	// padding
	public function addPKCS7Padding($data){
		$data = trim($data);
		$pad = $this->block_size - (strlen($data) % $this->block_size);
		if( $pad <= $this->block_size ) {
			$padStr = chr($pad) ;
			$data .= str_repeat($padStr, $pad);
		}
		return $data;
	}

	public function stripPKCS7Padding($data){
		$num = ord(substr($data,-1));
		return substr($data,0,-$num);
	}
}
$aes = new Aes('sama','128',false);
$en = $aes->encrypt('helloworldmydear');
$de = $aes->decrypt($en);

测试可行。

apache静态文件缓存的一些配置

在web应用中,合理利用缓存很重要。对于静态文件css/js/image/html等,可以使用http缓存。

每个浏览器都自带了 HTTP 缓存实现功能。您只需要确保每个服务器响应都提供正确的 HTTP 标头指令,以指示浏览器何时可以缓存响应以及可以缓存多久。

一般常用的会用Exprise,Last_Modified等头信息控制缓存。Cache-Control 标头是在 HTTP/1.1 规范中定义的,取代了之前用来定义响应缓存策略的标头(例如 Expires)。所有现代浏览器都支持 Cache-Control,因此,使用它就够了。

  • 每个资源都可通过 Cache-Control HTTP 标头定义其缓存策略
  • Cache-Control 指令控制谁在什么条件下可以缓存响应以及可以缓存多久

具体有一盘文章可以参考,http缓存,这儿就不多细说了。

既然缓存这么重要,那么服务器怎么配置很关键。这儿又有个常见服务器配置文件标准范例,找到你的服务器,参照着配置就行。

从上面来看,主要就两个标志很重要,Cache-Control和Etag,apache如何去配合使用这两个特性呢,假如我的根目录 的所有的css/js/image等静态文件都需要一个月的缓存,而且需要配合Etag的特性去保持更新。

首先看Cache-Control如何设置,在http.conf或者.htaccess(如果允许),加上一点代码:

<IfModule mod_headers.c>
    <File ~ "./(gif|jpg|jpeg|png|css|js)$">
          Header set Cache-Control  "max-age=2592000,public"
    </File>
</IfModule>

意思是说,如果加载了mod_headers这个模块,匹配文件以jpg,css,js等结尾的,在响应头内加上Cache-Control选项,缓存一个月(其实应该按照需求去设置)。用正则匹配的话可以使用FileMatch容器。

先看看不加这段代码。访问一张图片是怎样响应的

可以看到,响应里并没有cache-control,然后我们加上设置,重启,再访问一次看看

已经有了,一个月的缓存时间,很好。

cahce-control已经有了,还剩个Etag,其实apache默认就支持Etag,不用去设置,浏览器也会自动发送If-None-Match头去验证,很多工作都是自动的,但是它也有一些不同的生产方法。

apache支持FileEtag这个命令,官方介绍,它的参数主要有:INode,Size,Mtime,All,None

Inode是指每个文件的在文件系统里的i-node,Mtime指文件上次修改的时间,Size指大小,None就是不在响应里加上Etag标志。All就是FileEtag INode MTime Size

一般不用Inode,因为默认的ETag使用到的Innode会导致相同的文件在分布式服务器上产生的ETag不同。

看上图,我们访问后,产生的Etag : 303f-556eb6039b504,能看出来只使用了两个属性,如果我们想使用All去生成,那么可以:

<Direcrtory doucment_root>
     FileEtag All
</Directory>

重启,在访问一次,

很好发生变化了。变成 “9000000007bd9-303f-556eb6039b504″,显然多了一个。

平常开发中,可能会有不一样的需求,每个文件都可能需要不一样的缓存策略。

杂文一篇,有错望指出。

 

数据结构 – 反转链表

假如我们已经构造了如下链表:

头结点->6->5->4->3->2->1->null

现在要求反转链表,使其变成:

头结点->1->2->3->4->5->6->null

思路分析:既然是反过来,那么就从反过来后的第一个元素入手,即null,让第一个结点的next先指向null,类似: 6->null这个结构,继续这个思路,那下一步就是让5指向刚刚这个结构,依次类推,知道最后一个1指向2->3->4->5->6->null

代码:

// 2. 链表反转: 假设有链表6->5->4->3->2->1,反转链表成为1->2->3->4->5->6
function reverse(LineStruct $linelist) {
	// 第一个结点
	$pre = null;
	$leftNodes = $linelist->next;	
	while($leftNodes) {
		① $temp = $leftNodes->next;
		② $leftNodes->next = $pre;
		③ $pre = $leftNodes;
		④ $leftNodes = $temp;
	}
	$headNode = new LineStruct('',$pre);
	return $headNode;
}

先从第一遍开始分析,$leftNodes为6->5->4->3->2->1->null,之所以叫leftNodes,抽象表示剩下的结点,① 我们先用$temp保存6的下一个结点,即5->4->3->2->1->null,② 然后让6指向预先定义的pre,第一个当然是null,这个pre后面会改变。得到的结构是6->null,③ 接着我们用pre来保存这个结构,最后也是最重要的一部步,④将$leftNodes用$temp的值替换,那么下次循环就是从5这个结点开始了,先保存剩下的结点,,即4->3->2->1->null,然后5会先指向刚刚的pre,即6->null,变成5->6->null,这个结构一会再保存为pre,然后再讲$leftNodes置为$temp,直到最后一个$leftNodes = null,跳出循环,这时候的$pre就是1->2->3->4->5->6->null。

所以整个思路里最重要的就是保存当前结点的下一个结点,即保存剩下的结点。