用iptables的DNAT技术架设PPTP中继服务器

微软的PPTP协议是非常流行的VPN技术,它的指令控制部分使用1723端口上的TCP连接传输,而实际的VPN数据则使用GRE协议封装。如果你在一个防火墙的后面使用PPTP协议的VPN技术,那么你的防火墙必须支持PPTP穿隧才能正常使用。这是因为一般的防火墙都使用UDP或者TCP端口号的方式来生成和记录NAT对应关系,而GRE协议没有端口号。不过好在微软在GRE协议之上加了个东西,那就是PPP数据的Channel ID,利用这个东西,防火墙就可以根据它来记录NAT对应关系,把GRE报文转交给正确的内网主机。

现代的路由器一般都支持PPTP穿隧技术,所以基本无需自己设置就可以在防火墙后访问公网上的PPTP服务器建立VPN连接。另一方面,现在的商用路由器几乎都能识别1723端口,如果你在公司的路由上对1723端口做了DNAT映射,那么来自外网的PPTP请求也能正确地和防火墙后的VPN服务器建立连接。

如果使用Linux的iptables做软件防火墙,而没有使用路由器,我们依然可以建立起一个DNAT通到内网的PPTP服务器。这首先需要Linux内核加载ip_nat_pptpip_conntrack_pptp两个模块。可以用lsmod命令检查是否已经载入。

# lsmod | grep pptp

如果没有载入,可以使用modprobe加载内核模块

# modprobe ip_nat_pptp
# modprobe ip_conntrack_pptp

当然,为了做NAT,把IPv4 Forward打开也是非常必要的

# sysctl -w net.ipv4.ip_forward=1

接下来可以设置允许PPTP的TCP协议和GRE协议连入

# iptables -A INPUT -d $FIREWALL_IP -p tcp --dport 1723 -j ACCEPT
# iptables -A INPUT -d $FIREWALL_IP -p gre -j ACCEPT
# iptables -A FORWARD -d $PPTPD_IP -p tcp --dport 1723 -j ACCEPT
# iptables -A FORWARD -d $PPTPD_IP -p gre -j ACCEPT

最后配置DNAT规则完成转发,当然为了识别DNAT后的地址对应关系,转换地址后还需要把它加入MASQUERADE里。

# iptables -t nat -A PREROUTING -d $FIREWALL_IP -p tcp --dport 1723 -j DNAT --to-destination $PPTPD_IP
# iptables -t -A POSTROUTING -d $PPTPD_IP -p tcp --dport 1723 -j MASQUERADE

这样,当PPTP客户端拨号到$FIREWALL_IP上的时候,实际是后面的$PPTPD_IP服务器在提供服务。运行iptables的机器也可以视为PPTP中继服务器,向后面的服务转发数据。

阿里云9月1日事故的记录

事件经过

事情发生 9.1 11:40

9月1日上午11点多钟,我正登录服务器进行维护工作,因为前一天在服务器部署了新的seafile,还有搜索等功能需要手动开启。大约到了11:40的时候,朋友让我帮他测试一个IP的路由路径,我就在服务器上运行了mtr测试。然而mtr刚发送了几个数据包,就自己退出了。我很奇怪地再运行一次,系统却提示我无法运行mtr,没有那个程序。这还真是奇怪的事情,前几秒还在运行的程序,现在就没了。我以为是不是PATH环境变量出什么问题了,运行which mtr也找不到。于是决定重新安装一个,直接apt-get安装。结果问题又来了,apt-get提示我程序配置出错,要运行dpkg --configure修复。我再按提示运行,却看到提示说dpkg命令也不存在了。

我意识到服务器出现问题了,本着“重试、重启、重装”的三重原则,我选择了重启服务器。结果悲剧地无法启动了,连接管理终端发现机器卡在Booting from Hard Disk...就不动了。我的第一反应是保护自己的数据,马上到阿里云后台的磁盘管理中创建了一个磁盘快照。然后提交工单寻求技术支持。

提交工单 9.1 12:16

12:16我提交了工单,等着十几分钟没有回应,不过因为是个人网站,我就没有打电话催促。此时我还不知道阿里云发生了线上事故,只以为我的云服务器硬盘坏掉了,不过考虑到阿里云宣传的多重备份的硬盘管理策略,加上我又做了一个快照,我并不太担心自己的数据丢失。于是我提交完工单就和同事吃饭去了。

客服回复 9.1 12:50

回到办公室的时候,已经是下午一点多钟,阿里云工单有了进展,客服在12:49接手工单,在12:52给出事故原因:因云盾升级触发bug,导致少量文件被系统误删除。我们已经第一时间启动系统回滚。被误删除的文件正在陆续恢复,您无需进行手动恢复操作,请耐心等待。对您带来的不便我们深表歉意。

好吧,这时候我觉得阿里云的服务还不错,事故原因合情合理,客服也代表公司表达了歉意。甭管多大的软件公司,谁能不出点差错呢,这次也就和以前杀毒软件病毒库升级误杀系统文件导致开不了机一样。既然让我不要操作,我就不管了。

论坛炸锅 9.1 15:40

我本以为恢复一个误删的文件并不难,对于那些没重启的机器,既然云盾能把文件删了,自然可以统一往下分发文件;对于已经重启启动不能的云服务器,阿里完全可以用程序启一个等同系统的实例然后把用户的文件系统挂上去,再参照前面的方法恢复。

阿里云总共能用的系统镜像也没几个,统一恢复工作应该不会超过一个小时就能完成。结果事实证明我毕竟还是too young,时间过去了三四个小时,还是没有完成恢复。v2ex和阿里云官网论坛上哀鸿遍野,无数人在诉说自己被老板被客户骂到快死掉的过程。

等待了三个多小时后,阿里云工单上终于回复:您好,问题目前还在恢复中,有进一步进展,我们会随时和您反馈,对您带来的不便我们深表歉意,还请您耐心等待。

阿里云的官网则挂出了公告

尊敬的客户: 因云盾安骑士server组件的恶意文件查杀功能升级触发了bug,导致部分服务器的少量可执行文件被误隔离。系统在第一时间启动了回滚,目前被误隔离的文件已基本恢复。我们正在回访个别尚未恢复的客户,协助尽快恢复。对于受影响的客户,我们将立即启动百倍时间赔偿,并避免类似失误再次发生。我们深知这一失误对您业务带来的影响和损失,再次致以最深刻的歉意。

统一恢复完成 9.1 18:37

18:15阿里云工单回复Windows系统已经恢复,Linux系统还需要时间。18:37再次更新,说全部恢复完毕,并让我如果有问题随时反馈。然而我的服务器还是挂着的,我在18:43反馈了服务器的实际情况,表示并没有修复,不同意关闭工单,要求进一步处理。

收到阿里云通知邮件 21:09

时间又过去两个多小时,收到了阿里云的通知邮件:

如果您的ECS机器云盾进程已经退出,我们无法下发文件进行修复,请通过以下方式开启: Windows机器请启动 Alibaba Security Aegis Detect Service 服务 步骤: 1)打开计算机管理 2)选择 “服务” 3)找到 Alibaba Security Aegis Detect Service 服务并启动 4)如果找不到该服务,请尝试查找并启动 Alibaba Security Aegis Update Service 服务 Linux 机器 方案1: service aegis start 方案2: 进入/usr/local/aegis/aegis_client目录,选择高版本子目录,例如(aegis_00_79) 进入该子目录,运行 AliYunDun 进程

工单上并没有任何解释或者回复,我留言表示服务器挂了无法启动阿里云盾的进程。

最终解决方案 9.2 00:05

午夜钟声敲响不久,阿里云给出了适用我的最终解决方案:

如果您重启了主机,后台就无法正常恢复被隔离的文件了. 当前建议: 1. 登录主机控制台,对系统盘打个快照,等待100%完成, 2. 回滚系统盘至之前正常使用的快照点. 打快照的目的是如果当前系统中有重要数据,回滚后如果系统正常了,我们可以把您打的快照挂载到正常系统中,供您做数据恢复.

问题解决 9.2 01:04

终于在凌晨一点多的时候,问题得以全部解决,网站数据恢复成功,重新对外提供服务。

事件反思

客服回复不及时

遇到突发事故,阿里云的客诉量肯定大幅上涨,这个可以理解。但是阿里云似乎并没有采取什么有效手段来增加人手,及时回应客户的工单。提出问题后长达几个小时没有任何人回应,客服电话长时间无法接通,让客户自己干着急。

缺少必要的技术建议

我在个人网站一直挂掉,苦等到下午三点多钟的时候,同事反应公司的服务器也出现了系统命令丢失的问题。我的个人服务器在青岛,公司的服务器在北京,原本我以为只是阿里小范围测试时出现的问题,没想到居然北京机房也被波及。我立刻阻止他重启服务器,告诉他重启很可能会马上挂掉,因为操作系统的程序都被误删除了。然而阿里云的公告和故障申诉的工单中都没有提示用户这一点。

相信吐槽最多、损失最大的是商业用户,他们不指望你赔偿多少倍的云服务使用时间,就希望能尽快恢复服务器运行,甚至有的用户可能丢失一部分数据也可以接受。阿里云的技术人员完全有能力在事发一小时后整理出一份处理意见表,说明这次事故导致的系统程序损坏、用户数据损坏、系统无法启动等几种情况分别要怎么处理,可以最快恢复对外服务。

既然下午三点的时候云盾的误杀行为已经停止,进入恢复数据的阶段。此时投入应急的机器,配合客户紧急迁移业务程序到新的云主机上来恢复服务也不是不可行的。以我们公司的情况为例,用着阿里云的四台ECS,开几台新的云主机上线部署一次也就半小时到一小时的事情。

论坛上有人分享通过自行回滚到前一天的快照已经恢复服务,而阿里云官方在工单上要求用户不要操作,然后在长达十几个小时之后才给出回滚到前一天的快照,人工恢复数据的处理方案,真的是耗时太久了,损失也太大了。


经历这件事情知道云服务商宣传的99.999%的可靠度也就是个统计数字,自己中奖了就已经后悔莫及,经常备份数据或者设置备用机房才能让业务更安全。

最后发现今天各新闻网站全然没有报道此次事故,不知道是因为事故太小还是因为阿里太大。现在只好坐等阿里的100倍赔偿。

SPDY真的快吗?

SPDY真的快吗?

Google SPDY协议(现已加入HTTP2.0草案)宣称能够提升浏览器速度一倍,HTTPS同SPDY协议在前端加载速度上的对比如下(参考极客公园):

8bb3b4b0e21d86701f1ebd880768ec84.gif

针对该优点,小伙伴进行了对比测试,发现http协议SPDY协议在前端响应速度上没太大出入。

Protocol 1 2 3 4 5 Avg
http 35.91 35.80 35.86 36.95 37.80 36.46
spdy 36.26 36.21 36.24 36.27 36.30 36.26

注:所有统计时间均在chrome下禁用缓存并开启调试模式进行,时间单位为秒。

看到这个测试,心都凉了半截,为什么spdy的加载时间和http的加载时间相差无几,完全没有Google宣称的那般美好,甚至大部分的耗时比http还长,感觉Google忽悠了整个世界,再也无法相信美帝国主义的公司。

面对他的这个测试报告,我只能说一定是开门的方式不对。

先来说结论:SPDY无法改善用户的上网带宽,用这么大的图片来测试,时间基本都花在传输上了,自然难以看出区别。

传统浏览器慢在哪里

要了解SPDY到底快在哪里,先得看HTTP协议性能都消耗在了哪里,干脆我们看看传统的浏览器都需要把时间花到哪里:

  1. DNS查询时间(把域名转成IP地址,一般从几毫秒到几秒不等,移动网络中此过程有时需要十几秒)
  2. TCP握手时间(取决于你和服务器之间的Ping有多高,TCP的三次握手决定了每建立一个TCP连接都需要三倍单程时间)
  3. TCP慢启动窗口时间(刚建立的TCP连接不会以最大速度发送数据,数据传输速度会有一个慢慢增长的过程)
  4. HTTP请求发送时间(一般的GET请求头有几百字节到几千字节)
  5. 等待服务器处理时间(读取磁盘,查询数据库,运行服务端程序逻辑等等,一般几毫秒到几秒不等)
  6. HTTP回应接收时间(下载服务器返回的数据,一般文本内容只有几KB到几十KB,图片等内容可能有几MB)
  7. 浏览器解析处理时间(处理DOM,排版,屏幕绘图等等)

那么Google又针对以上几种时间消耗又分别做了什么优化呢?

  1. Chrome浏览器会Cache下来DNS查询结果,Google甚至鼓励你用Google的DNS服务器。
  2. 网页总有许多资源需要加载,包括CSS、JS、图片等,为了并行加载提高效率,现代浏览器会为每个域名建立6个TCP连接,每个TCP连接都需要经历握手过程。
  3. 每个TCP连接都会经历慢启动的过程,Google也没办法,因为TCP的慢启动策略是操作系统管理的。不过现代浏览器都实现了HTTP/1.1的Keep-Alive支持,能用一个连接发送多个请求,虽然由于复杂度的原因Pipeline基本都没有实现,但也能有效避免像HTTP/1.0那样每传输一项资源都重新建立一次连接。
  4. 第4、第6点Chrome是无法优化的,因为HTTP协议就那么定的。
  5. 第5点掌握在网站开发者手中,Google虽然无力改变别的网站,但他可以把自家的网站优化好,比如YouTube、Picasa、Gmail等等Google应用都有做诸多优化,响应速度也是业内领先的。
  6. 浏览器解析处理方案,Chrome下了不少工夫,先是借力Webkit的轻巧高效,又有引入强大的带JIT的V8 JavaScript解释器,后来更是搞出了Blink渲染引擎,可谓Webkit的再优化版。

SPDY到底快在哪里

当Google发现自己把能做的都做了之后,只能向他们掌控不了的领域动刀,前面时间消耗的第2、3、4、6点都值得从协议上优化,于是Google抛出了SPDY协议。

  1. SPDY支持多路复用。并行化的好处显而易见,Google Chrome为每个域名开6个TCP就会让服务器需要维护的TCP连接数量上涨到三倍,而且每个连接都要经历三次握手和慢启动过程。SPDY干脆就只用一个TCP连接,这样三次握手和慢启动过程只走一遍,因为支持多路复用,所以SPDY仍可以享受并行化加载的好处,Pipeline与否也显得无关紧要。
  2. SPDY支持优先级设定,对于渲染页面而言,关键的CSS和图标资源可以优先加载,非关键的CSS和JS以及图片可以低优先级加载,这样可以帮助浏览器提高渲染效率。
  3. SPDY支持HTTP头压缩,并强制启用正文压缩,能有效减少请求和回应的数据量,优化HTTP的数据传输量。
  4. 此外,SPDY还有Server Push这种需要服务器开发人员支持的大杀器帮助提升速度。一般浏览器都是先下载HTML,然后看到里面有引用某CSS再去请求这个CSS,解析CSS看到里面需要一张图片再去请求这张图片。SPDY协议允许网页开发者直接把这些次生资源主动Push到浏览器中,这样当浏览器解析HTML发现需要一个CSS的时候,会发现这个CSS已经下载好了,不用再发请求下载了,解析CSS发现需要某张图片的时候,发现它也已经下载好了。
  5. 对于第二次来访者,考虑到次生资源可能已经有浏览器缓存,SPDY允许服务器发送Server Hint,告诉浏览器本网页需要哪些资源,没Cache的直接下载,不用等解析到才来请求。这样也能有效提升速度。

SPDY慢在哪里

俗话说有得必有失,SPDY也是有额外开销的,这些开销会减慢它的速度。那么SPDY的开销在哪里呢?和HTTP协议相比,SPDY的额外开销只有一个,那就是它全程使用SSL/TLS,存在证书交换时间。和HTTPS差不多,每次浏览器连上SPDY服务器之后,先会收到一个长达几KB的SSL安全证书,然后浏览器还要计算证书签名是否合法,并搜索一遍证书吊销记录中有没有这个证书的指纹信息,然后才开始生成密钥传输数据。即便是现代计算机,这个过程也会花掉几十到几百毫秒的时间。

正确的测试方法

由于Server Push和Server Hint这种大杀器都需要服务端跟踪记录每个网页会引用到的资源,并确定用户浏览器是否首次来访,不容易准备测试用例也不容易在自己的服务中实施,暂时先不考虑。要对照测试SPDY和HTTP的性能差异,至少应该用接近真实网页的例子,或者用比较能突出并行化优势的例子。像前面小伙伴测试的例子,主要性能开销都在网络传输二进制图片文件之上,SPDY的性能优势被全部淹没在了统计误差之中。

极客公园文章中用GIF演示的例子是一个偏向SPDY优势的例子,大量的小图标文件非常适合高度并行化的传输,而且几百字节的HTTP Header和大量不到1KB的图片文件相比,额外的负载显得如此严重,SPDY的头压缩功能可以很有效地降低传输数据量,HTTP的并行度也远无法和SPDY相提并论,非常容易看到SPDY在性能提升上的效果。考虑到实际的确会有一些网页会出现这样的情况(比如国家/地区选择页),这个例子也不算太背离实用场景。当然,真正的前端高手遇到这种情况会选择使用CSS Image Sprite技术来减少HTTP请求量,提升加载速度。不过如果HTTP本身能支持高度并行化地加载,CSS Image Sprite技术也不过是奇技淫巧罢了。

我制作了类似极客公园的例子,大家可以自己测试比较HTTP版本SPDY/HTTP2版本的加载速度差异。(我的测试服务器同时支持SPDY和HTTP2,具体选用哪个协议取决于浏览器)

下面是我测试出来的数据

Protocol 1 2 3 4 5 Avg
http 8.52 7.65 8.25 7.48 8.00 7.98
spdy 3.06 2.73 2.69 2.87 2.74 2.82

我取用南方周末的首页制作了另一个测试用例,这是典型的新闻类网站首页的情况,大家可以自行比较HTTP版本SPDY/HTTP2版本的性能。下面是我的数据:

Protocol 1 2 3 4 5 Avg
http 2.94 2.09 2.04 3.21 2.71 2.60
spdy 3.15 2.79 2.66 1.99 2.35 2.59

这份数据告诉我们,像南方周末这样新闻类网站,大图片一堆一堆的,SPDY并不能提升多少访问速度,至少速度的提升是无法感知的。用Page Speed Insight测试之后可以发现,南方周末网站首页的新闻图片大都可以在不损失画质的前提下压缩超过80%的文件大小。

总结

SPDY协议无法改善用户的上网带宽,以数据传输为时间开销的网站测试无法突显出SPDY协议的优势,不过即便是这种情况,SPDY的性能表现也不会比传统的HTTP慢。SPDY协议是Google在自家网站优化到极致之后,把手伸到HTTP协议上而祭出的神器。借它优化自家网站的访问速度之前,还是先把其它该优化的点都充分做好吧。

PDO prepare足以防止SQL注入吗?

这是一篇翻译的文章,可以点击查看原文


简单来说不能,PDO prepare无法防御全部的SQL注入攻击。比如一些潜在的边缘情况。

我调整了这个答案以适应PDO的场景……

完整的答案是不太容易,这篇文章证明了这种攻击的可能。

攻击实现

那么,接下来我来演示这种攻击……

$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));

某些情况下,上面的代码会返回多于一行数据。让我们剖析一下这里到底发生了什么:

1. 选择一个编码集

$pdo->query('SET NAMES gbk');

要让这种攻击生效,我们需要服务编码和连接编码两者都把'以ASCII形式编码,也就是0x27并且这种编码中要存在某个字符以ASCII的\也就是0x5c结尾。事实上,MySQL5.6默认支持的编码中有五种符合这一要求:big5cp932gb2312gbk还有sjis。我们这里用gbk举例。

现在,请格外注意这里的SET NAMES指令,它改变了服务器上的字符集。当然还有另一种方法修改字符集,我们后面会谈到。

2. 要嵌入的字符串

我们用于演示SQL注入的字符串以0xbf27开头。在gbk编码中,这是个不合法的多字节字符,而在latin1编码中,它是字符串¿'。注意,不论latin1还是gbk编码,0x27都是半角单引号'字符。

我们选择这个字串的原因在于,如果我们对它调用addslashes(),我们会在'字符前插入一个ASCII码的\字符,即0x5c。然后我们得到字符串编码是0xbf5c27,在gbk编码中,这是两个字符0xbf5c,后面接着0x27。换句话说,这是一个合法的字符后面跟着一个没转义的'。不过我们没调用addslashes(),那么会进行下一步。

3. $stmt->execute()

这里有一件重要的事情需要搞清楚的是,PDO在默认情况下并不会真的对语句做prepare,而是会模拟这一行为(对于MySQL)。也就是说,PDO会在内部构建查询语句,对每个绑定的字符串调用mysql_real_escape_string()(MySQL的C语言API中的函数)。

C语言API调用mysql_real_escape_string()addslashes()的不同点在于前者知道MySQL连接的字符集,所以它可以正确地根据服务器要使用的字符集进行转义。然而,正因为这一点,MySQL客户端会使用latin1进行转义——因为我们从来没有通知它切换字符集的事情。我们的确告诉服务器我们在用gbk,可是客户端仍以为是latin1

这样的情况下调用mysql_real_escape_string()时会插入反斜杠\,而那个挂单的'就会被转义出来!实际上,如果我们以gbk编码查看$var的值是:

縗' OR 1=1 /*

以上就是进行攻击的环境要求。

4. 生成的查询语句

这部分只是为了完整,下面是生成的SQL查询语句:

SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1

恭喜,你刚刚完成了一次针对PDO prepare程序的攻击。

简单的修复方法

值得注意的是你可以简单地通过禁用prepare语句模拟功能来防御这种攻击:

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

这样一般导致PDO执行真正的prepare操作。(数据会独立于SQL语句传送给服务器。)不过还是得小心PDO会在本地的MySQL无法完成prepare时悄悄地恢复使用模拟prepare语句的状态。具体情况在MySQL手册中已然列出,请注意选择对于你服务器版本的手册。

正确的修复方法

这里真正的问题在于我们没有调用MySQL的C语言API中的mysql_set_charset(),而是使用了SET NAMES语句。如果正确调用了mysql_set_charset(),那么使用2006年之后发布的MySQL版本都将安全。

如果你仍在使用更早版本的MySQL,那么mysql_real_escape_string()中的一个bug会导致它把类似我们例子中的不合法的多字节字符串当作单字节字符串加以处理,这样即便客户端正确地指定了连接编码集和服务器编码集,仍会存在被攻击的风险。这个bug在MySQL 4.1.205.0.225.1.11中得以修复。

不过最糟的问题在于PHP 5.3.6之前版本的PDO压根就没暴露C语言API中的mysql_set_charset()方法,所以早于此版本的用户根本不可能阻止这种类型的攻击。现在PDO以一个DSN参数的形式暴露了这个接口,应该用这种方法取代SET NAMES……

其它好办法

话说回来,这种攻击能生效的前提条件是数据库连接使用了薄弱的字符编码。utf8mb4是一种不存在此类弱点的字符集编码方法,而且它能支持所有的Unicode字符——你应该考虑切换到这种编码上。可惜只有MySQL 5.5.3以上版本支持这一编码。另一个可以考虑的替代字符集编码是utf8,它也不存在前述弱点,而且能支持所有Unicode基本多文种平面中的字符

此外,你也可以启用NO_BACKSLASH_ESCAPES SQL模式,这会改变mysql_real_escape_string()的内部行为(也包括其它东西)。当这个模式启动时,0x27会被替换为0x2727而不是0x5c27,这样转义后输出的字符在所有的薄弱的编码字符集中都不会和前面的字符连成合法的存在(比如0xbf27转义出来还是0xbf27),最终服务器就会拒绝这个非法的字符串。不过请看看@eggyal的回答以了解这种SQL模式下的别的漏洞(尽管与PDO无关)。

安全的例子

以下例子是安全的:

mysql_query('SET NAMES utf8');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

因为服务器期待utf8……

mysql_set_charset('gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

因为已经正确地设置和服务器匹配的客户端字符集。

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

因为关掉了prepare模拟。

$pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=gbk', $user, $password);
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

因为已经正确地设置字符集。

$mysqli->query('SET NAMES gbk');
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "\xbf\x27 OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();

因为MySQLi总是执行真正的prepare操作。

总结一下

如果你:

  • 使用了现代版本的MySQL(晚期版本的5.1,所有的5.5,所有的5.6等等)并且使用了PDO的DSN里的charset参数(PHP ≥ 5.3.6)

或者

  • 没用有弱点的连接字符集编码(比如只使用utf8latin1ascii等等)

或者

  • 启用了NO_BACKSLASH_ESCAPES SQL模式

那么你100%安全。

其它情况下,即使你用了PDO的prepare方法也依然存在SQL注入风险

补充

我正在慢慢编写一个补丁程序让未来版本的PHP默认不要使用模拟prepare方法。导致我进展缓慢的原因在于实在太多测试阻止我这么做。其中一个问题是模拟prepare功能只会在执行的时候返回SQL语法错误,然而真正的prepare可能会在进行prepare的时候就回报错误,这会带来许多麻烦(这也是导致那些测试出错的部分原因)。

为nginx配置安全的SSL证书

最近Google放了一个大招,在所有基于Chromium的浏览器中把SHA-1签名认证的https证书全部加入了连接可能不具备私密性警告。于是绿色的锁头变成灰色,还带一个黄色的大三角。如下图:

1125923241.jpg

想了解关于这个警告的含义和来龙去脉,请移驾Fredrik Luo的博客,上面有又详细又通俗的解释。现在是时候该升级一下服务器的TLS安全设置了。

对于使用SHA-1签名的证书的网站,唯一的方法是生成更高强度的哈希并重新签名。可以用下面的命令生成新的CSR,并提交给你的CA进行reissue操作:

openssl req -new -key theOriginPrivate.key -out example.com.csr -sha256

好吧,尽管安全要求只是至少升级到SHA-2,但干脆直接上SHA-256或者SHA-384算了,反正碰撞的可能性更低,而且现代计算机做个正向计算简直没难度。致使用CNNIC做根证书签名的用户:你们还是赶快换CA吧,CNNIC整个证书链都是SHA-1的,光升级你的CSR没用。

一般的域名验证证书只需通过电子邮件就能验证完成,和申请新证书基本一样。建议取得新的签名证书文件之后不要着急安装,等几个小时再安装。因为新的证书有效期是从重新签名当时起算的,可能有的客户的设备时钟不准,马上换装可能导致他们的设备报告证书不合法。一般新的证书签发后,旧的证书还能继续用三天,三天后负责任的CA会把你的旧证书写到已吊销列表中。

换装了新证书后,顺便找了个叫ssllabs的网站测试https的安全情况,用他们主页上的Test Your Server功能可以测试你的网站https的安全性和兼容性。测试结果里Signature algorithm一栏透露了证书的签名算法,我的已经是SHA256了。

650315878.jpg

nginx使用OpenSSL作为底层加密库,所以影响安全性的Cipher Suites设定至关重要,如果设置得安全性极高,可能失去对早期版本浏览器的兼容性而流失用户;反之如果设置的安全性过低,又会陷用户于易受攻击的状态并被新版浏览器嫌弃。在cipherli.st上提供了包括nginx在内的常用Web服务器安装设置,可惜这些设置对早期版本的浏览器兼容得不太好,几经周拆和尝试,我终于敲定使用以下设置:

ssl_certificate /etc/ssl/public.crt;
ssl_certificate_key /etc/ssl/private.key;
ssl_dhparam /etc/ssl/certs/dhparam.pem;
ssl_ciphers EECDH+AESGCM:AES256+EECDH:EDH+AESGCM:AES256+EDH:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;

其中第三行那个dhparam的设置,是因为nginx会自动使用OpenSSL默认的1024位的DH Pool,这在现在已被认为是不安全的,所以需要自己生成一个新的2048位的DH Pool。

openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048

本来cipherli.st推荐的是4096位的DH Pool,这当然能更安全,但无奈Java 7并不支持超过2048位的DH Pool,所以只好退而求其次,所幸2048位的DH Pool也算阶段性安全。

上面的配置并不支持Windows XP上的IE6,考虑到IE6默认设置只能用已经被证实有重大安全问题的SSL3.0,而坚持不升级的用户多半也不知道https为何物,更不知道怎么把TLS1.0打开,所以还是放弃这些用户吧。