我开发了一个基于 Beancount 的账本托管服务 HostedBeans,欢迎大家来了解纯文本复式记账并试用我的服务。
归档 2013 年 5 月

读书:编码

前些天,我花了差不多300元来买了一些书。应该说以前买书一直被坑了很久,当时因为认识的大牛没几个,没人推荐,只能是自己去书店找,找到哪本算哪本。大部分都是坑爹的国产书,不得不说国产书的质量普遍不如国外的——想必能漂洋过海来到中国的书应该都算是经典吧。因为鸟语水平不理想,读不下原著,只好看翻译的,译本的一个问题就是语言非常之僵硬,可以说是“英式中文”,不过有人说这不重要,技术书籍要以严谨为第一要素。

后来我就聪明多了,网购发展起来了,我会在网上买大牛们推荐的经典,顺便对比一下书评,被坑得也少了。
其实两三年来我一直在辽宁省图书馆借书看,免费借,挺划算的。应该说借书和买书还是有很大区别的,在图书馆看到一本书哪怕有10%对我有用,我也会借来看——反正不借白不借。而买书的话我得确定它全书都是值得反复读的才肯掏钱。

这次买的六本书,按对我的难易程度:

  • MongoDB权威指南
  • 程序员的数学
  • 编码——隐匿在计算机软硬件背后的语言
  • 计算机网络——自顶向下方法与Internet特色
  • 深入理解计算机系统(第2版)
  • 程序员的自我修养——链接、装载与库

把这个书单发给龙哥(某奔三十却还没有妹纸的程序员抠脚大叔)的时候,龙哥问我这是要造电脑么。

龙哥给我的印象就是一个躲在微软搭建的华丽城堡里的……嗯——奔三十却还没有妹纸的抠脚大叔。当然,用龙哥的话说,这又有什么关系呢?

我对原理性的细节总是很感兴趣,不仅仅是计算机。霍金的《时间简史》等书我的读过,宇宙如何运行,微观粒子如何作用,可能永远也不会和我发生关系,除了能在和民科对骂的时候加点底气,可能真的没什么用了。

同样我也知道我不可能有机会从三极管开始设计一款芯片,我也不会使用机器指令,手工汇编,甚至我也不大可能使用汇编语言以及C语言。但我觉得这很有趣。

我通常会把书拿来一遍一遍地看,泛读一遍最多几个小时,然后再读一遍,每一次都会理解到新的东西,直到没有新东西了,才算看完了,但过几个月也许我会再捡起来。

《Mongo…》只花了半天就看得差不多了,这只是一本参考性质的工具书,对官网文档的一个中文翻译,当时我看它不贵也就买了。

《编码…》这书我打开看了一遍,其他四本暂时只看了个开头。

《编码…》它自底向上地描述了一台计算机的构造过程,从继电器到逻辑门,加法器,随机存储器(RAM), CPU, 字符终端,汇编器。
从最简单的导线,开关,电磁铁开始,每一步都在使用上一步设计好的零件来组装,看完不由得感叹原来计算机的构造如此简单,只是遵从着几个基本的原理,但其思路却又如此精密。作为第一个示例,本书用了144个继电器(三极管)和十几个开关以及灯泡,实现了一个具有输出和输出的8位加法器。昨天去淘宝看了一下,继电器1元一个,三极管0.1元一个,看来自己造一个加法器也不是没有可能。

我想说这书其实是零基础的,你并不需要有多少基础知识,因为它是自底向上,自成体系的——我真的说不出需要什么基础,仅有的一点电路现学也够了。

在我看来,本书可分为三个部分:

  • 如何将数据编码为计算机可以理解的形式
  • 如何用基本的电路组成芯片实现自动化地计算
  • 如何通过构造更高级的编程语言提高编码效率

第一部分(如何将数据编码为计算机可以理解的形式)除了让我了解到了多一点细节,应该说对我没什么帮助,这部分应该就是面向零基础读者的吧,无非是进制换算,以及将数字和字符编码为二进制。

第二部分(如何用基本的电路组成芯片实现自动化地计算)则让我眼前一亮,把我带进了一个新的世界。其实之前我也知道逻辑门,但很难想象当今复杂的计算机是如何用基本的逻辑门构成的。
所以说抽象是最重要的工具,计算机中每一层都是对上一层的抽象,一开始可能会把逻辑门里的导线都画出来,后面就会用一个方块来代替加法器了。抽象也许是人类最伟大的创造,就好像你不能指望狗把一沓人民币抽象成骨头。

第三部分(如何通过构造更高级的编程语言提高编码效率)则是填补了我很多的知识空白。之前一直对机器指令以及汇编语言的定位很模糊,现在我算是理解了他们的合理性和必要性。
机器指令中需要直接使用操作数的内存地址,而汇编语言使用了助记符代替机器指令的编号,用标识符代替了内存地址,并且可以对地址进行计算。从汇编语言到机器指令的过程就叫汇编,在汇编器出现之前,是需要手工汇编,计算内存地址的。而汇编语言和机器指令都依赖于具体的CPU指令集,所以才有了后来高级编程语言的出现。

计算机可谓人类构造的最精密的系统之一,但我觉得它仍不及网络复杂,因为对芯片而言,大部分行为都是十分确定,可预测的。
而网络则增添了更多变数——事实上网络几乎是围绕着错误处理构造的。

前一阵我想写一篇日志,科普互联网是如何工作的,但写了一半就感觉写不下去了。虽然只是些基础的知识,估计有机会读我日志的各位都知道,但想要写出来,则并不简单。也许正是这样,霍金这等尖端物理学家,却还要写科普书。

与《编码…》不同,《网络…》一书是自顶向下的,内容量比较大,估计要看很久。

不过再难恐怕也比不上《操作系统…》和《链接…》,尤其《链接…》一书差不多已经把程序的每个字节都研究透了。

PHP进阶:1.安全

我决定写一系列教程:PHP进阶——一个论坛系统的实现,这是第一章。

1.0 安全

我们将安全放到了最前边,对于Web应用来说,安全的重要性不言而喻,如果你编写的程序存在漏洞,攻击者将有可能取得服务器的控制权,进行破坏,甚至利用你的站点进一步散播木马。

有些人认为测试性的代码,位于内网的应用,可以暂时不必关心安全,这是非常不负责任的。你很难预见这段代码什么时候会被作为你的应用的一部分,保险的做法是,为你写下的每一段代码负责,通过本章提出的几项原则,这并不麻烦。

  • SQL 注入
  • Shell 注入
  • Eval
  • XSS
  • CSRF
  • 关注官方通告
  • 最小权限原则
  • 中间人攻击
  • Dos 攻击
  • 隐藏信息

SQL 注入

如果你的应用需要将一个包含用户输入的字符串作为代码(如SQL)执行,那么攻击者可以通过构造巧妙的输入,来将额外的代码注入到要执行的字符串中,实现攻击.

下面举一个 SQL 注入的例子, 如果有如下的用户验证的代码:

$result = $db->query("SELECT * FROM `users` WHERE (`name` = '{$_POST['user']}') and (`passwd` = '{$_POST['passwd']}')");
if($result->fetch())
    echo "登录成功";

这段代码从 users 表中查询用户名为 $_POST[‘user’] 且密码为 $_POST[‘passwd’]的记录,如存在这样的记录,就认为登录成功。

但攻击者可以通过将 user 和 passwd 分别构造为如下的字符串进行攻击:

admin
1' OR '1'='1

这样一来,实际执行的SQL变成了这个样子:

SELECT * FROM `users` WHERE (`name` = 'admin') and (`passwd` = '1' OR '1'='1')

就可以绕过验证,实现以管理员登录.

产生SQL注入的原因在于单引号在 SQL 中具有特殊含义,单引号标识着一段字符串的开始和结束,如果用户输入中含有单引号,那么该字符串就会提前结束,剩下的部分就会作为SQL命令来解析.

所以,我们有必要对进入 SQL 语句的所有用户输入进行转义,所谓转义就是让代码中具有特殊含义的字符,失去特殊含义,仅仅表示它本身.

具体来说,我们在SQL中需要转义的字符有:

  • 反斜杠 \
  • NUL \0
  • 换行 \n
  • 回车 \r”
  • 单引号 ‘
  • 双引号 “
  • Ctrl+Z \x1a

还有一些和字符集相关的字符(在特定的字符集中,会有一些同义字).

所幸我们不必自己做这些工作,MySQL提供了一个名为 mysql_real_escape_string() 的函数用于将字符串转义,以安全地嵌入SQL.

而 PDO[1] 里同样有 PDO::quote() 完成同样的工作。

值得注意的是 mysql_real_escape_string() 和 PDO::quote() 的行为有细微的区别,前者只是转义字符串,后者在转义后还会在字符串前后加上单引号。

经过修改的代码会是这样:

$user = $db->quote($_POST['user']);
$passwd = $db->quote($_POST['passwd']);
$result = $db->query("SELECT * FROM `users` WHERE (`name` = {$user}) and (`passwd` = {$passwd})");
if($result->fetch())
    echo "登录成功";

如果攻击者继续构造类似之前的注入字符串,那么数据库会去查询用户名为 admin 且密码为 1' OR '1'='1 的记录,数据库仅仅将这段注入字符串视为它字面的意思,不会作为命令来解析,这样攻击者就不会得逞了.

所以,记住我们的第一个原则:

如果你需要将一段字符串作为代码来执行,那么要对所有进入字符串的用户输入进行转义

用户输入不仅仅在于 $_GET 和 $_POST, 包括HTTP Header, Cookie, 文件都是用户输入。

[1]: PHP Data Object, PHP的新式数据库访问接口, 后文会讲解.

更多SQL注入的例子参见: http://php.net/manual/zh/security.database.sql-injection.php

Shell 注入

如有如下代码:

shell_exec("rm /data/files/{$_POST['file']}");

这段代码会根据用户输入,删除 /data/file/ 下的指定文件.

既然你已经知道了我们的第一个原则,显然我们要对输入进行转义。
于是我们找到了 escapeshellarg(), 它会对进入Shell的参数进行转义,添加空格。

经过修改的代码:

$arg = escapeshellarg("/data/files/{$_POST['file']}");
shell_exec("rm {$arg}");

如果攻击者输入 xxoo;rm -rf /, 那么实际执行的命令是:

rm '/data/files/xxoo;rm -rf /'

可以看到攻击者没有成功。

但是攻击者可以继续尝试输入 ../../etc/passwd, 这下你傻眼了, 因为执行了:

rm '/data/files/../../etc/passwd'

相当于:

rm '/etc/passwd'

删掉了系统的用户数据库.

所以,我们除了要对用户输入进行转义之外,还需要 从逻辑上对输入进行检验

在前面SQL注入的例子中,这种检验是不必要的,因为我们知道数据库里面用户的密码不可能恰巧等于攻击者构造的恶意字符串。

但在这个Shell注入的例子中,我们有这个必要:

if(strstr($_POST['file'], "/") !== false)
{
    echo "文件名非法";
}
else
{
    $arg = escapeshellarg("/data/files/{$_POST['file']}");
    shell_exec("rm {$arg}");
}

这样我们便安全了,如果文件名包含了斜杠,程序会拒绝执行命令.

在Linux下,文件名可以使用除斜杠外的所有字符,如果需要迁移到Windows下,我们还需要做更多工作。

所以请记住我们的第二个原则:

不要相信用户的输入,考虑到每一种可能性

你最好把编写代码的过程,看作“证明这段代码不会出错”的过程。

Eval

包括 PHP 在内的大多数脚本语言都提供了类似 eval 的函数,可以将字符串作为代码来执行。同样,在使用 eval() 时我们也需要考虑注入攻击。

但不同于 SQL 和 Shell, 使用 eval() 的大多数场合都是可以避免的,你需要的可能是匿名函数等机制(后文会介绍), 所以在这里不推荐使用 eval() 函数。

XSS

Cross-site scripting, 跨站脚本攻击,或者你可以认为是 HTML 注入,这种攻击并非针对服务器,而是针对访客。

大多数网页的输出都是 HTML, 这意味着你的 PHP 程序的输出会作为 HTML 代码来显示在用户的浏览器中,而 HTML 中嵌入的 JS 脚本则会被用户的浏览器执行。

例如一个论坛系统显示帖子的代码:

<?php
$post = getPostData($_POST['id']);
?>
<html>
    <head>
        <title><?= $post['title'];?></title>
    </head>
    <body>
        <?= $post['content'];?>
    </body>
</html>

如果有一个帖子的内容是下面这样呢:

<span style="color: red;">帖子内容</span>

显然文字会被显示为红色,这意味着如果帖子中含有 HTML 代码,将会被直接执行,在这个例子中这段 HTML 代码可以认为是无害的,但如果是嵌入一段脚本呢:

<script>alert("帖子内容");</script>

浏览器会执行一段 JS 代码!如果你认为这除了开个玩笑外也是无害的,那么请看下面的输入:

<script>
    $.post("/topic/create/", {"title": "点进来看一看啊~", "content": "<script>" + $("body script:first").html() + "</script>"});
</script>

这段代码会自动发布一个新帖子,其内容就是这段代码本身!这样以来,这个“无害的玩笑”会在整个论坛传播开。

XSS 有持久性和非持久性(反射型)之分,前面介绍的例子属于持久性 XSS, 即已经将攻击代码保存在数据库中,其他人访问这些信息时,HTML代码就会被执行。

而非持久性的意思就是注入代码并未被持久性的保存,而是存在于 URL 中,如:

http://xxoo.xo/show.php?id=<script>alert();</script>

对应的 PHP 代码:

if(!is_numeric($_GET['id']))
    echo "{$_GET['id']} 不是一个数字";

如果你轻易点击了这个连接,那么你将会被执行一段脚本,危险性不再复述。

你想说不会有人点这么显然的链接?其实它可以很隐蔽:

http://xxoo.xo/show.php?id=%3Cscript%3Ealert()%3B%3C%2Fscript%3E

甚至:

http://xxoo.xo/show.php?id=%3C%73%63%72%69%70%74%3E%61%6c%65%72%74%28%29%3B%3C%2F%73%63%72%69%70%74%3E

既然是注入 HTML,那么对策并不复杂:对所有输出到 HTML 的数据进行转义,使用 htmlspecialchars(), 如:

if(!is_numeric($_GET['id']))
{
    $id = htmlspecialchars($_GET['id']);
    echo "{$id} 不是一个数字";
}

好吧,其实前面那么多铺垫这是为了让你了解 XSS 的重要性,当下它们甚至比 SQL 注入更加流行。

CSRF

Cross-site request forgery, 跨域请求伪造。

浏览器本身为 JS 制定的一个“跨域保护”规则,保证了 JS 仅能访问当前站点下的资源。

规则即一个域名下的 JS 无法使用 XHR[4] 访问另一个域名下的资源。

但这个限制并不包括 <img>, <script> 等标记的 src 属性,而且这也不现实,因为一个站点难免引用其他站点的图片等资源。

现在很多站点都使用类似于 /user/logout/ 的链接来实现注销登录的功能。如果有这样一段代码:

<img src="//xxoo.xo/user/logout/" />

那么如果用户访问了这个页面,它在 xxoo.xo 网站就会被注销登录,无论当前站点是不是 xxoo.xo !

这样攻击是非常隐蔽的,因为如果图片加载失败了也只不过是一个小叉而已,还可以通过 CSS 隐藏起来。

上面的例子也许可以认为是无害,因为被注销登录也算不得什么大不了的事情,但如果你继续使用 GET 方式响应非修改性的请求的话,事情就没那么简单了。

记住: 所有修改性的操作都使用POST方式

[2]: XMLHttpRequest, JS 无刷新请求技术的 API 接口。

关注官方通告

有时候,漏洞并不在于我们编写的代码,而在于我们所使用的软件本身,如PHP, 或者Linux, MySQL等等。

虽然这些软件的代码审核和发布过程是非常严谨的,但也难免有漏网之鱼。一些严重的漏洞会在一天之间被传播,利用和修复。

对此我们没有什么对策,毕竟这些软件不是我们自己写的,我们能做的但只有:

  • 使用官方的稳定发行版
  • 及时关注官方通告,安装补丁

在PHP领域,有很多问题是官方在反复提醒,但仍广泛存在的,我们在此指出几点:

关闭 Register Globals

这是 php.ini 中的一个选项(register_globals), 开启后会将所有表单变量($_GET和$_POST)注册为全局变量.

看下面的例子:

if(isAuth())
    $authorized = true;
if($authorized)
    include("page.php");

这段代码在通过验证时,将 $authorized 设置为 true. 然后根据 $authorized 的值来决定是否显示页面.

但由于并没有事先把 $authorized 初始化为 false, 当 register_globals 打开时,可能访问 /auth.php?authorized=1 来定义该变量值,绕过身份验证。

该特征属于历史遗留问题,在 PHP4.2 中被默认关闭,在 PHP5.4 中被移除。

关闭 Magic Quotes

对应 php.ini 中的选项 magic_quotes_gpc, 这个特征同样属于历史遗留问题,已经在 PHP5.4 中移除。

该特征会将所有用户输入进行转义,这看上去不错,我们之前提到过要对用户输入进行转义。

但是 PHP 并不知道哪些输入会进入 SQL , 哪些输入会进入 Shell, 哪些输入会被显示为 HTML, 所以很多时候这种转义会引起混乱。

最小权限原则

让我们试想如果 PHP 出现了一个验证漏洞,导致可以任意执行Shell命令,这将会多么恐怖。事实上这种漏洞在各种服务器软件上时有发生。

虽然我们无法阅读和修改 PHP 的源代码,但我们可以通过给予 PHP 最小的权限,来减少漏洞被利用后的损失。

很多教程在遇到文件权限问题的时候,会很不负责任地让你将文件夹权限设置为 777, 即最宽松权限。

在Linux下,每个文件,每个进程都对应着一个用户,非 root 用户只能控制自己的文件,自己的进程,自己的进程创建的文件也是属于自己的。

每个文件有 9 个权限位,分别对应 自己, 同组用户, 其他用户读取, 修改, 执行 权限

基于这种机制,我们可以将服务器的不同功能划分为不同的用户,实现隔离,即使一个服务被攻击者控制,也不会影响到其他的服务。

事实上各个发行版也是这么做的,Apache2, PHP-FPM[2] 大多是以 www-data 或类似的用户运行的,即使被拿到权限,也只能控制网站的部分,无法执行需要 root 权限的系统级命令。

我们可以进一步对权限进行细分,如果应用不需要写入文件,那么给 Web 目录设置只读权限就可以了。

储存静态资源(图片, CSS, JS等)的目录,不要允许 PHP 执行。

甚至让每一个站点都以单独的用户来运行[3].

如果服务器仅提供 Web 服务,那么防火墙仅开放 80 端口。

对于数据库以及其他服务,我们也需要给予应用最小的权限,为每个应用建立单独的账户,只赋予每个应用读写自己的数据库的权限。

[3]: PHP 在 Apache2 下可能依附于 Apache2进程,在 Nginx 则大多用 PHP-FPM 方式运行。

[4]: Apache2 可使用 apache2-mpm-itk, PHP-FPM 可使用配置文件中的 user 选项。

中间人攻击

在 Web 领域,中间人攻击即服务器和用户之间,存在着一个可以双向篡改数据包的 中间人,它有可能是网络中的一个路由节点,也可能是一个钓鱼网站。

说实话这已经是网络传输层面的问题了,和 PHP 没多大关系,解决中间人攻击的唯一途径就是使用 https 协议,SSL 除了可以防御中间人攻击外,还可以让你的数据以加密的形式传输(HTTP是明文传输的), 目前网络上已经有了一些免费的证书颁发机构(CA), 如 StarSSL.

DoS 攻击

Denial of Service, 阻断服务攻击,通过大量垃圾请求迫使服务器无法工作。

如果你的网站让攻击者无处下手,那么攻击者可能会使出最野蛮的攻击方式,DoS, 比如使用脚本快速访问你的站点,让你的服务器不堪重负。

首先你应当避免你的站点出现性能瓶颈,尽量通过缓存(后文会讲解)来提供内容,减少请求处理时间。

设置 Web 服务器的最大进程数,避免进程过多导致内存不足。

还可以使用如 DDoS deflate[5] 的脚本,自动对攻击 IP 进行封禁,这对单点 DoS 非常有效。

如果攻击者不计成本地使用流量“堵”你的线路,那么你可以选择一些知名的 CDN 服务来过滤请求,隐藏服务器的真实 IP 地址。

隐藏信息

虽然隐藏信息是一种比较消极的防御手段,但也能为攻击者添加一些障碍。

值得隐藏的信息包括:Web服务器版本,脚本引擎(PHP)版本,错误信息等等。

笔记: 循环, 递归, 迭代, 遍历

本文首发于SegmentFault, http://segmentfault.com/q/1010000000199577#a-1020000000199630

表示&#8221;重复&#8221;这个含义的词有很多, 比如循环(loop), 递归(recursion), 遍历(traversal), 迭代(iterate).

循环

循环算是最基础的概念, 凡是重复执行一段代码, 都可以称之为循环. 大部分的递归, 遍历, 迭代, 都是循环.

递归

递归的定义是, 根据一种(几种)基本情况定义的算法, 其他复杂情况都可以被逐步还原为基本情况.

在编程中的特征就是, 在函数定义内重复调用该函数.

例如斐波那契数列, 定义F(0)=1, F(1)=1, 所有其他情况: F(x)=F(x-1)+F(x-2).

所有大于1的整数经过有限次的反推之后都可以转换到两种基本情况. 而在编程中, 算法则是这样的:

int F(x)
{
    if(x==0 || x==1)
        return 1;    //这里是退出递归的条件, 以保证在有限次递归后能够得到结果
    return F(x-1)+F(x-2);    //转化为更为基本的情况, 重复调用自身进行计算
}

迭代(数学)

迭代在数学和编程中有不同的含义.

数学方面是指在循环的基础上, 每一次循环, 都比上一次更为接近结果.

例如下面是一个迭代的例子.

int result = 0;
for(int i=0; i>10; i++)
    result += i;    //每一次循环之后, result都更加接近结果45

有很多数学问题, 都是迭代算法, 如牛顿迭代法(求平方根).

迭代(编程)

按顺序访问一个列表中的每一项, 这在很多编程语言中表现为foreach语句:

$arr = [1, 2, 3, 4];
foreach($arr as $i)
    echo $i;

遍历

按一定规则访问一个非线性的结构中的每一项, 强调非线性结构(树, 图). 而迭代一般适用于线性结构(数组, 队列).

结论

  • 循环(loop) &#8211; 最基础的概念, 所有重复的行为
  • 递归(recursion) &#8211; 在函数内调用自身, 将复杂情况逐步转化成基本情况
  • (数学)迭代(iterate) &#8211; 在多次循环中逐步接近结果
  • (编程)迭代(iterate) &#8211; 按顺序访问线性结构中的每一项
  • 遍历(traversal) &#8211; 按规则访问非线性结构中的每一项

这些概念都表示“重复”的含义, 彼此互相交叉, 在上下文清晰的情况下, 不必做过于细致的区分.

参考

精子生于 1995 年,英文 ID jysperm.

订阅推送

通过 Telegram Channel 订阅我的博客日志、产品和项目的动态:

王子亭的博客 @ Telegram


通过邮件订阅订阅我的博客日志、产品和项目的动态(历史邮件):

该博客使用基于  Hexo  的  simpleblock  主题。博客内容使用  CC BY-NC-ND  授权发布。最后生成于 2023-12-20.