五大受损, 全面解析PHP的糟糕设计(2)

来源:oschina 作者:oschina
  

    >>    Object+verb 对 verb+object: base64_decode, str_shuffle, var_dump versus create_function,     recode_string

    >>    参数顺序: array_filter($input, $callback) versus array_map($callback, $input), strpos($haystack, $needle) versus array_search($needle, $haystack)

    >>    前缀混乱: usleep vs microtime

    >>    Case insensitive functions vary on where the i goes in the name.

    >>    大概一半的数组函数以 array_ 开头. 剩下的不是.

>>    厨房水槽. 库包括:

    >>    绑定 ImageMagick, 绑定 GraphicsMagick (ImageMagick的派生), 少量的几个函数能检测 EXIF 数据 (其中ImageMagick已经可以做到)

    >>    解析 bbcode 的函数, 一些非常特殊的标记, 被几个少量的论坛包使用.

    >>    太多 XML 包. DOM (OO), DOM XML (not), libxml, SimpleXML, “XML Parser”, XMLReader/XMLWriter, 和一大砣我不能认出的东西就省略了. 当然会有些不同, 你可以自由的弄清晰它们的区别.

    >>    绑定了两个特别的信用卡处理器, SPPLUS 和 MCVE. 什么?

    >>    三种访问 MySQL 数据库的方式:  mysql, mysqli, 和 PDO 抽象的一些东西.

C 影响

    它需要拥有的自己的符号. PHP 是个高层的, 动态类型的语言. 然后大量的标准库的部分仍然只是围绕 C APIS 的薄层封装, 伴随着下面的东西: 

>>    "Out" 参数, 尽管 PHP 可以返回 ad-hoc 哈希或毫不费力的返回多参数. 

>>    至少一打的函数是为了获取某子系统的最近一次错误(见下文), 尽管 PHP 已存存异常处理功能8年了.   

>>     有个 mysql_real_escape_string, 尽管已有个具有相同参数的 mysql_escape_string, 仅仅因为它是 MySQL C API 的一部分.

>>    全局行为却是非全局功能的(如 MySQL). 使用多个 MySQL 连接需要显示的对每个函数调用传递连接句柄.

>>    包装器真的, 真的, 真的很薄. 例如, 调用了 dba_nextkey 而没调用 dba_firstkey 將出现段错误.

>>    有一堆的 ctype_* 函数 (如 ctype_alnum) 映射类似名称的 C 字符函数,  而不是如, isupper. 

Genericism

    如果函数相做两件略有不同的事, PHP 就搞出两个函数.

    你怎样反向排序? 在 Perl 中, 你可以用 { $b <=> $a}. 在 Python 中, 你可能用 .sort(reverse = True). 在 PHP 中, 有个特别的函数叫 rsort().    

>>    那些看起来像 C error 的函数: curl_error, json_last_error, openssl_error_string, imap_errors, mysql_error, xml_get_error_code, bzerror, date_get_last_errors, 还有其它的吗?

>>    排序函数: array_multisort, arsort, asort, ksort, krsort, natsort, natcasesort, sort, rsort, uasort, uksort, usort

>>    文本检索函数: ereg, eregi, mb_ereg, mb_eregi, preg_match, strstr, strchr, stristr, strrchr, strpos, stripos, strrpos, strripos, mb_strpos, mb_strrpos, plus the variations that do replacements

>>    有大量的别名: strstr/strchr, is_int/is_integer/is_long, is_float/is_double, pos/current, sizeof/count, chop/rtrim, implode/join, die/exit, trigger_error/user_error…

>>    scandir 返回一个当前给出目录的文件列表. 而不是(可能有益)按返回目录顺序返回, 函数返回一个已排序的文件列表. 有个可选的参数可以按字母逆顺返回. 这些用于排序很显然很不够.

>>    str_split 將字符串拆成等长的块. chunk_split 將字符串拆成等长的块, 然后用个分隔符连接.

>>    读取压缩文件需要一套单独的函数, 取决于格式. 有六套函数, 它们的 API 都不同, 如 bzip2, LZF, phar, rar, zip, 和gzip/zlib

>>    因为使用参数数组调用函数是如此的别扭(call_user_func_array), 所以有些配套的像 printf/vprintf 和 sprintf/vsprintf. 它们做相同的事, 但一个带多个参数, 另一个带参数数组.

文本

>>    preg_replace 带 /e (eval) 标志的將用待替换的字符串替换匹配的部分, 然后 eval 它.    

>>    strtok 的设计显然是和 C 函数等效的, 由于很多原因, 已被认为是个坏注意. PHP 可以轻易的返回一个数组(而这在C中别扭), 很多的hack strtok(3) 用法 (修改字符串某处), 在这里不能使用.

>>    parse_str 解析查询字符串, 从函数名看不出任何迹象. 而它会 register_globals 并转存查询字符串到本地范围变量中, 除非你传递一个数组来填充. (当然, 什么也不返回)

>>    碰到空分隔符, explode 会拒绝分割. 每个其它的字符串拆分实现采取这种作法的意思应该是把字符串应拆分成字符; PHP有一个拆分函数, 令人迷惑的称为 str_split 而却描述为 "將字符串转成数组". 

>>    格式化日期, 有 strftime, 像 C API 处理本地语言环境一样. 当然也有 date, 完全不同的语法而仅用于 English. 

>>    "gzgetss -- 获取 gz 文件的行指针并去除 HTML 标记." 知道了这一系列函数的概念, 让我去死吧.

>>    mbstring

    >>    都是关于 "multi-byte", 解决字符集的问题.

    >>    仍然处理的是普通字符串. 有个单一的全局"默认"的字符集. 一些函数允许指定字符集, 但它依赖于所有的参数和返回值.

    >>    提供了 ereg_* 函数, 但这些都被废弃了. preg_* 很幸运, 用一些 PCRE-specific 标记, 它们能理解 UTF-8. 

系统和反射

>>    有一大堆的函数, 聚焦于文本和变量. 压缩和提取仅是冰山一角. 

>>    有几种方式让PHP动态, 咋一看没有什么明显的不同或相对好处. 类工具不能修改自定义类; 运行时工具取代了它并能修改自定义的任何东西; Reflection* 类能反射语言的大部分东西; 有很多独特的函数是为了报告函数和类的属性的. 这些子系统是独立, 相关, 多余的吗?

>>    get_class($obj) 返回对象的类名称. get_class()返回被调用函数中的类的名称. 撇开这些不说, 同一个函数会做完全不同的事情: get_class(null)... 行为象后者. 因此面对一个随机的变量, 你不能信任它. 惊讶吧!

>>    stream_* 类允许实现自定义的流对象给fopen和其它的内建的类似文件处理的东西使用. 由于几个内部原因, "通知" 不能被实现. 

>>    register_tick_function 能接受闭包对象. unregister_tick_function 不行; 相反, 它会抛出错误, 抱怨闭包不能转换成字符串.     

>>    php_uname 告知你当前操作系统相关东西. 

>>    fork 和 exec 不是内建的. 它们来自 pcntl 扩展, 但默认不包含. popen 不提供 pid 文件.

>>    session_decode 用于读取任意的 PHP session 字符串, 但仅当有个活跃的 session 时才工作. 它转存结果到 $_SESSION 中, 而不是返回它的值.      

杂项 

>>    curl_multi_exec 不改变 curl_error 当出错的时候, 但它改变 curl_error. 

>>    mktime 的参数是有顺序的: hour, minute, second, month, day, year

数据操纵

    程序什么都不是, 除了咀嚼和吐出数据以外. 大量的语言围绕着数据操纵设计, 从 awk 到 Prolog 到 C. 如果语言无法操纵数据, 它就无法做任何事. 

数字

>>    Integers 在32位平台是是有符号32位数. 不像PHP的同时代者, 没有自动 bigint 提升. 因此你的数学运算可能会由于CPU体系结构结果不一样. 你唯一选择大整数的方式是使用 GMP 或 BC 包装函数. (开发者可能已经建义加入新的, 单独的,64位类型. 这真是疯了.)

>>    PHP支持八进制数语法, 以0开头, 因此如 012 是10. 然而, 08变成了0. 8(或9)和任何接下来的数字消失了. 01c是个语法错误.

>>    pi 是个函数. 或者有个常量, M_PI. 

>>    没有幂操作符, 只有 pow 函数.

文本

>>    无Unicode支持. 只有ASCII工作是可靠的, 真的. 有个 mbstring 扩展, 上面提过的, 但会稍被打击.

>>    这意味着使用内建的string函数处理UTF-8文本会有风险.

>>    相似的, 在ASCII外, 也没有什么大小写比较概念. 尽管有扩展版本的大小写敏感的函数, 但它们不会认为 é 等于 É.

>>    你不能在变量中内插keys , 如, "$foo['key']"是个语法错误. 你也不能 unquote it (这样会产生警告, 无论什么地方!), 或使用 ${...}/{$...}

>>    "${foo[0]}"是对的. "${foo[0][0]}"是个语法错误. 糟糕的拷贝类似 Perl 的语法 (两个根本不同的语议)?

数组

    呕, 骚年.

>>    这家伙扮演list数据类型, 操作hash, 和排序set, 解析 list, 偶尔会有些奇怪的组合. 它是怎样执行的? 以何种方式使用内存? 谁知道? 不喜欢, 反正我还有其它的选择.

>>    => 不是操作符. 它是个特别的结构, 仅仅存在于 array(...) 和 foreach 结构中.

>>    负值索引不工作, 尽管 -1 也是个和0一样的合法键值.

>>    尽管这是语言级的数据结构, 但没有简短语法; array(...)是简短语法. (PHP 5.4 带来了"literals", [...].) 

>>    => 结构是基于 Perl , Perl允许 foo => 1 而不用引号.  在PHP中, 你这么做会得到警告; 没有无需引号创建 hash 字符串键值的方式.

>>    数组处理函数常常让人迷惑或有不确定行为, 因为它们不得不对 lists, hashes, 或可能两者的结合体做运算. 考虑 array 分组, "计算arrays的不同部分". 

 

1 $first  = array("foo" => 123, "bar" => 456);
2  
3 $second = array("foo" => 456, "bar" => 123);
4  
5 echo var_dump(array_diff($first, $second));

 

    这段代码將做什么? 如果 array_diff 將参数以 hashes 看待, 它们明显是不同的; 相同的keys有不同的值. 如果以list看待, 它们仍然是不同的; 值的顺序不同.

    事实上 array_diff 认为它们相等, 因为它以 sets 对待: 仅仅比较值, 忽略顺序.

>>    同样, array_rand 随机选择keys时, 也有奇怪的行为, 这对大多数需要从列表中挑出东西的用例没什么帮助. 

    尽管大量PHP代码依赖key的顺序:

 

1 array("foo", "bar") != array("bar", "foo")
2  
3 array("foo" => 1, "bar" => 2) == array("bar" => 2, "foo" => 1)

 

>>    如果两个数组混合的话, 会发生什么? 我留给读者自己弄清楚. (我不知道)

>>    array_fill 不能创建0长度的数组; 相反它会发出警告并返回 false.

>>    所有的(很多的...) 排序函数就地操作而什么都不返回. 想新建一个已排序数组的拷贝, 没门; 你不得不自己拷贝数组, 然后排序, 然后再使用数组.

>>    但 array_reverse 返回一个新数组.

>>    一堆被排序的东西和一些键值对听起来像是个某种强大的处理函数参数的方式, 但, 没门.

非数组 

>>    标准库包含 "快速哈希", "特定的强类型"的hash结构OO实现. 然, 深入它, 有4类, 每种处理不同的键值对类型组合. 不清楚为什么内建的数组实现不能优化这些极其普通情况, 也不清楚它相对的性能怎样.

>>    有个 ArrayObject 类 (实现了4个不同的接口) , 它包装数组让它看起来像对象. 自定义类可以实现同样的接口. 但只有限的几个方法, 其中有一半不像内建的数组函数, 而内建的数组函数不知道怎样对ArrayObject或其它的类数组的类型操作. 

函数

>>    函数不是数据. 闭包实际上是对象, 但普通的函数不是. 你甚至不能通过它们裸名称引用它们; var_dump(strstr) 会发出警告并猜测你的意思是字符串字面量, "strstr". 想辨别出字符串还是"函数"引用, 没门. 

>>    create_function 基本上是个 eval 的包装者. 它用普通的名字创建函数并在全局范围安装它(因此永远不会被垃圾回收---不要在循环中使用!). 它实际上对当前上下文一无所知, 因为它不是闭包. 名字包含一个 NUL 字节, 因此永远不会与普通函数冲突 (因为如果在文件的任何地方有 NUL的话,  PHP 的解析器会失败).

>>    Declaring a function named __lambda_func will break create_function—the actual implementation is to eval-create the function named __lambda_func, then internally rename it to the broken name. If __lambda_func already exists, the first part will throw a fatal error.

其它 

>>    对 NULL 使用 (++) 生成 1. 对 NULL 用 (--) 生成 NULL. 

>>    没有生成器.

Web 框架 

执行环境

>>    一个单一共享文件 php.ini, 控制了 PHP 的大部分功能并织入了复杂的针对覆盖什么与何时覆盖的规则. PHP软件能部署在任意的机器上, 因此必须覆盖一些设置使环境正常, 这在很大程序上会违背像 php.ini 这样的机制的使用.

>>    PHP基本上以CGI运行. 每次页面被点击, PHP 在执行前, 重编译整个环境. 就连 Python 的玩具框架的开发环境都不会这样.

>>    这就导致了整个 "PHP 加速器" 市场的形成, 仅仅编译一次, 就能加速PHP, 就像其它的语言一样. Zend, PHP的幕后公司, 將这个做为它们的商业模式.

>>    很长时间以来, PHP的错误默认输出给客户端 -- 我猜是为开发环境提供帮助. 我不认为这是真相, 但我仍然看到偶尔会有mysql 错误出现在页面的顶部. 

>>    在 <?php ... ?>标签外的空白, 甚至在库中, PHP以文本对待并解析给响应 (或者导致 "headers already sent" 错误). 一个流行的做法是忽略 ?>关闭标签.

部署

    部署方式常常被引述为PHP的最高级部分: 直接部署文件就可以了. 是的, 这比需要启动整个进程的 Python 或 Rury 或 Perl 要容易. 但 PHP 留下了许多待改进的地方.

    我很乐意以应用服务器的方式运行Web应用程序并反向代理它们. 这样的代价最小, 而好处多多: 你可以单独管理服务器和应用程序, 你可以按机器的多或少运行运行多个或少量应用进程, 而不需要多个web服务器,你可以用不同的用户运行应用, 你可以选择web服务器, 你可以拆下应用而无需惊动web服务器, 你可以无缝部署应用等等. 將应用与web服务器直接焊接是荒谬的, 没有什么好的理由支持你这么做.

>>    每个 PHP 应用程序都使用 php.ini . 但只有一个 php.ini 文件, 它是全局的; 如果你在一个共享的服务器上, 需要修改它, 或者如果你运行两个应用需要不同的设置, 你就不走运了; 你不得不向组织申请所有必须的设置并放在应用程序, 如使用 ini_set 或在 Apache 的配置文件或在 .htaccess设置. 如果你能做的话. 可能 wow , 你有大量的地方需要检查以找出怎样获取已设置的值.

>>    类似的, "隔离"PHP应用的方法也不容易, 它依赖于系统的其它部分. 想运行两个应用程序,想要不同的库版本, 或不同的PHP版本本身? 开始构建另一人Apache的拷贝吧.

>>    "一堆文件"方案, 除了使路由像只病重的笨驴外, 还意味着你不得不小心处理白名单或黑名单, 以控制什么东西可访问, 这是因为你的 URL 层次也就是你的代码树的层次. 配置文件和其它的"局部模块"需要C之类的东西守护以避免直接加载. 版本控制系统的文件(如 .svn) 需要保护. 使用 mod_php , 使得文件系统的所有东西都是潜在的入口; 使用应用服务器, 仅有一个入口, 并且仅通过 URL 控制调用与否.

>>    你不能无缝的升级那堆以 CGI-style 运行的文件, 除非你想要应用崩溃和出现未定义行为, 当用户在升级的间歇期点击你的站点时.

>>    尽管配置 Apache 运行 PHP 很"简单", 仍然会有一些陷阱. 而 PHP 文档建议使用 SetHandler 使得 .php 文件以 PHP方式运行, AddHandler 看起来运行良好, 然而事实上会有问题.

    当你使用 AddHandler, 你在告知 Apache "以 php 执行它" , 这是一个可能的处理 .php 文件的方式. 但! Apache 对文件的扩展名不这样认为. 它被设计为能支持如, index.html.en 这样的文件. 对于 Apache , 文件可以同时具有任意数量的扩展名.

    猜想, 你有个文件上传的表单, 存储一些文件到公共目录中. 确保没人能上传 PHP 文件, 你仅仅检查文件不能有.php 扩展名. 所有的攻击需要做的只是上传以 foo.php.txt 命名的文件; 你的上传工具不会看出问题,  Apache 会认为它是个 PHP, 它会很高兴的执行.

    这里不是 "使用原始文件名" 或 "没有更好的验证"导致的问题; 问题是你的web服务器要被配置用来运行任何旧代码, 使得PHP "容易部署".  这不是理论上的问题; 我已发现很多实际的站点有类似的问题了.

缺失的特性

我认为所有这些都是以构建一个Web应用为中心的. 对PHP看起来很合理, 是它的销售卖点之一, 它是 "Web语言", 理应有它们.

>>    无模块系统. PHP就是模版.

>>    无 XSS 过滤器. htmlspecialchars 不是 XSS 过滤器.  

>>    无 CSRF 保护. 你必须自己做. 

>>    无通用标准的数据库API. 像PDO这类东西不得不包装每个特定数据库的API, 分别抽象不同部分.

>>    无路由系统. 你的站点结构就是你的文件系统结构. 

>>    无认证或授权.

>>    无开发服务器.

>>    无交互调试模式.

>>    无一致的部署机制; 仅仅"拷贝所有文件到服务器中".

安全 

语言边界

    PHP的蹩脚安全机制可能会放大, 因为它利用某语言拿出数据, 又把它转存到另一个中. 这是个坏注意. "<script>" 可能在SQL中意味着什么都不是, 但在HTML中就很是了.

    让情况更糟糕的是通常有人哇哇喊到 "你的输入要消毒". 那完全错误; 你不可能有什么魔法使块数据完全"干静". 你需要做的就是对语言说: SQL使用占位符, 进程孵化使用参数列表, 等等.

>>    PHP公然鼓励 "消毒": 有个数据过滤扩展可以做到.

>>    所有的 addslashes, scripslashes, 和其它的 slashes相关的东西都是废物, 毫无用处. 

>>    我只能告诉你这么多, 无法安全的孵化进程. 你仅能通过shell执行字符串. 你的选择是疯狂的转义, 并希望默认的shell使用正确的转义, 或手动的 pcntl_fork_exec 和 pcntl_exec.

>>    所有的转义命令和转义参数存在大致相同的描述. 注意在Windows中, 转义参数不工作 (因为它假设成 Bourne shell 语议), 转义命令仅仅用空格替换一堆标点符号, 因为没人能搞清楚 Windows 命令转义行为 (它可能默默的破坏你试图做的任何事情). 

>>    原始的内建 MySQL 绑定, 仍然广泛使用, 它无法创建 prepared statements. 

    直到今天, PHP 文档关于SQL注入的建议还是让人抓狂的做如类型检查, 使用sprintf 和 is_numeric, 在每个地方手动的使用mysql_real_escape_string , 或在每处手动使用 addslashes (这个"可能更有用"!) 这样的实践. 并没有提到 PDO 或 参数化, 除了在用户评论中有点线索.  至少在两年以前, 我就有具体的向 PHP dev 抱怨过了 , 他被惊动了, 而页面却从未变过.

Insecure-by-default

>>    register_globals. 它被默认关闭的,而在5.4中去除了. 我不在乎. 

>>    include 接受 HTTL URLS. 和上面一样. 

>>    Magic quotes. So close to secure-by-default, and yet so far from understanding the concept at all.

核心 

    PHP解释器本身就有一些恼人的安全问题.

>>    2007年的时候, 解析器有个整数溢出漏洞. 修复始于 if(size > INT_MAX) return NULL; 从那以后就走下坡路了. (对于那些不需要使用C的人: 曾经, INT_MAX 是适合变量最大整数. 我希望你能从这里搞清楚其余的东西.)

>>    最近, PHP 5.3.7 包括了个 crypt() 函数, 有个漏洞让任何人可以用任何密码登录. 

>>    PHP5.4是容易遭受拒绝服务攻击,因为它需要Content-Length头(任何人都可以设置),并试图分配更多内存。这是一个坏主意。

    我可以挖掘更多, 但重点不是这有很多X漏洞 -- 是软件就有bugs, 无论如何都有. 这些自然是令人咋舌. 我并没有特意寻找这些; 但在过去的几个月里, 它们自己送上门来了.

总结

    一些评论会理所当然的指出我没得出任何结论. 好吧, 我是没有结论. 如果你一路看到了这里, 我假设一开始你就同意我了 :)

    如果你仅了解PHP而对学习其它东西感兴趣, 可以看看 Python 教程, 尝试 Flask 这个为web准备的家伙. (我不是它的模版语言的铁杆粉丝, 但它确实很好的完成了这些工作.) 它將你的应用分成多个部分, 但它们看起来仍然是一致的. 我可能稍后会写个关于这个的贴子; 旋风般的介绍整个语言和不同于这里所说的web堆栈. 

    之后或对于更大的项目, 你可能需要 Pyramid, 一个中等规模的框架, 或者是 Django, 一个构建站点的复杂的框架, 如 Django站点.

英文原文 转载请注明出处 OSCHINA.NET


时间:2012-04-20 20:07 来源:oschina 作者:oschina 原文链接

好文,顶一下
(0)
0%
文章真差,踩一下
(0)
0%
------分隔线----------------------------


把开源带在你的身边-精美linux小纪念品
无觅相关文章插件,快速提升流量