月度归档:2011年12月

wordpress升级到3.3引起的血案

某位哥哥因为看着wordpress3.3的升级提示激动了,一不小心就将它升级了,升级后一切正常,只是之前可用的附件上传功能现在不能用了,显示报错如下:

Warning: touch() [function.touch]: SAFE MODE Restriction in effect.
 The script whose uid is 10041 is not allowed to access /tmp owned by uid 0 in ......

各种纠结……

依据提示我们知道是由于PHP开启了安全模式的原因,最简单的办法:修改php.ini文件,将safe_mode设置为off,将安全模式关闭。

只是这是生产环境,关闭安全模式不太靠谱。只能换方案。
依据刚才的错误提示,我们知道是安全模式在检查执行程序的文件的UID与需要写入的文件夹的UID不一样,这是因为在PHP的安全模式中,对于部分函数是需要检查被操作的文件或目录是否与被执行的脚本有相同的 UID(所有者),很不幸,touch就是这样的函数之一。当然,有这个规则就应该有跳过这个规则的办法,在php.ini文件中,我们可以设置safe_mode_include_dir参数,多个路径以冒号隔开(Windows是分号),在此参数内设置的目录及其子目录都将会越过 UID/GID 检查。

除此之外,安全模式相关的还有一个open_basedir设置项,PHP 所能打开的文件限制在open_basedir指定的目录树,包括文件本身。本指令不受安全模式打开或者关闭的影响。

safe_mode_include_dir和open_basedir指定的限制实际上是一个前缀,而非一个目录名。这也就是说“safe_mode_include_dir = /tmp”将允许访问“/tmp和“/template”(如果它们存在的话),。如果希望将访问控制在一个指定的目录,那么请在结尾加上一个斜线,例如:“safe_mode_include_dir = /tmp”。

这两个与本次安全问题相关的内容都添加了/tmp和当前操作的目录,发现依旧报错。

追根溯源,从源码中开始吧,如果对于PHP源码不太熟悉,那么我们可以从本次事件的关键点safe_mode_include_dir开始。

在PHP源码中搜索safe_mode_include_dir,除了与此配置项相关的初始化操作,我们可以找到验证此项的函数php_check_safe_mode_include_dir。现在,我们可以用GDB debug的方法查看,给php_check_safe_mode_include_dir函数增加断点,运行一个你认为一定会经过此参数检查的PHP函数调用,如fopen等。运行,发现会在断点处停下,如果我们运行一个只包含了touch函数的代码,会发现程序会执行完,即touch函数根本就没有调用php_check_safe_mode_include_dir函数,即在安全模式下,根本无法跳过UID的检测。

或者我们直接查看touch函数的PHP源码实现,其代码在/ext/standard/filestat.c文件649行。在其代码中我们可以清楚的看到其实现过程在处理了相关参数后,就直接判断完全模式和检测UID了,如下代码:

PHP_FUNCTION(touch)
{
 
...
 
/* Safe-mode */
if (PG(safe_mode) && (!php_checkuid(filename, NULL, CHECKUID_CHECK_FILE_AND_DIR))) {
RETURN_FALSE;
}
 
/* Check the basedir */
if (php_check_open_basedir(filename TSRMLS_CC)) {
RETURN_FALSE;
}
 
...
 
}

找到了问题的根源,也许是PHP的BUG,也许是其它考量,待确认。

那么这个问题怎么解决呢?

比较wordpress3.3和wordpress3.2的代码,我们发现wordpress在处理附件上传时最终都会调用wp_handle_upload函数(/wp-admin/include/file.c)。在此函数中wordpress3.3版本多了一句:

$tmp_file = wp_tempnam($filename);

在wp_tempnam函数中,最终会通过touch生成临时文件,从前面我们可以知道我们暂时无法解决在此环境下的touch函数的问题,那么我们只能适应这个函数,也就是说我们需要将新创建的临时文件的目录UID和执行的PHP文件的UID设置为一样,那么这个临时文件的目录是否可以改变?我们查看wp_tempnam函数,发现其最先取的临时目录的地址为常量WP_TEMP_DIR的值,此值默认是没有设置的,如果这个值没有才会取其它值。此时,解决方案就浮出水面了:在wp-config.php文件中设置常量WP_TEMP_DIR的值为网站目录下某个UID和PHP脚本一样UID的目录即可。

附gdb bt显示的内容

#0 php_check_safe_mode_include_dir (path=0x86f4fcc "/tmp/aaa43243.tmp")
at /home/martin/project/c/phpsrc/main/fopen_wrappers.c:319
#1 0x0829d97b in php_plain_files_stream_opener (wrapper=0x85eb624, 
path=0x86f4fcc "/tmp/aaa43243.tmp", mode=0x86f6b38 "a", options=4, 
opened_path=0x0, context=0x86f5560)
at /home/martin/project/c/phpsrc/main/streams/plain_wrapper.c:991
#2 0x0829842d in _php_stream_open_wrapper_ex (
path=0x86f4fcc "/tmp/aaa43243.tmp", mode=0x86f6b38 "a", options=12, 
opened_path=0x0, context=0x86f5560)
at /home/martin/project/c/phpsrc/main/streams/streams.c:1867
#3 0x08229b73 in php_if_fopen (ht=2, return_value=0x86f5544, 
return_value_ptr=0x0, this_ptr=0x0, return_value_used=1)
at /home/martin/project/c/phpsrc/ext/standard/file.c:909
#4 0x0817b088 in phar_fopen (ht=2, return_value=0x86f5544, 
return_value_ptr=0x0, this_ptr=0x0, return_value_used=1)
at /home/martin/project/c/phpsrc/ext/phar/func_interceptors.c:418
#5 0x0831b9a1 in zend_do_fcall_common_helper_SPEC (execute_data=0x8727eb8)
at /home/martin/project/c/phpsrc/Zend/zend_vm_execute.h:313
#6 0x082f4bc6 in execute (op_array=0x86f545c)
at /home/martin/project/c/phpsrc/Zend/zend_vm_execute.h:104
#7 0x082d2256 in zend_execute_scripts (type=8, retval=0x0, file_count=3)
---Type <return> to continue, or q <return> to quit---
at /home/martin/project/c/phpsrc/Zend/zend.c:1194
#8 0x08281b70 in php_execute_script (primary_file=0xbffff2a0)
at /home/martin/project/c/phpsrc/main/main.c:2225
#9 0x08351b97 in main (argc=4, argv=0xbffff414)
at /home/martin/project/c/phpsrc/sapi/cli/php_cli.c:1190

Scheme学习笔记2: 输入和输出

本文包括以下内容:

作为一门语言,它需要提供与外设交互的方式,在各种语言中都提供了输入输出,Scheme语言也不例外。Scheme语言的输入输出功能,是在C基础上的一种封装。

read和write

输入输出中我们常用是读和写,对应读与写,在Scheme中有read和write。
read是读操作,它将 Scheme 对象的外部表示转换为对象本身。
read的标准格式为:(read port)
其中port参数可以省略,此时它使用的默认是current-input-port的返回值。

write是写操作,它的标准格式为:(read obj port)
write过程的作用是向给定端口port输出obj的内容。其中输出内容的反斜线、双引号等会被反斜线转义。write的参数中port参数可以省略,默认情况下使用current-output-port的返回值。

这里的默认值我们可以对应到标准输入和输出。从ZOJ的第一题我们看下read的使用。
如下代码:

ZOJ第一题(zoj1001):

(define (main)
 
(let ((a (read)) (b (read)))
 
(if (not(eof-object? a))
(begin
(display (+ a b))
(newline)
(main)
)
)
)
 
)
 
(main)

题目很简单,就是输入两个数,输出和。这里的难点是对于输入结束的判断,在C语言中scanf函数有一个EOF的判断,而在Scheme语言中,我们通过判断输入的输入的值是否为eof来判断,其对应的判断过程为eof-object?(嗯,问号也是调用过程的组成部分,个人非常喜欢这种表达方式),在判断不为结束时继续递归调用,实现C语言中的while循环。

字符操作

对于字符的读写操作,我们常用的三个过程如下:

read-char
标准格式: (read-char port)
读取一个字符,并将port指针指向下一个字符。
read-char的参数中port可以省略,默认情况下使用current-input-port的返回值。

write-char
标准格式: (write-char port)
write-char过程的作用是向给定端口port输出的内容。
write-char的参数中port可以省略,默认情况下使用current-output-port的返回值。

peek-char
标准格式: (peek-char port)
获取一个字符,但并不将port指针指向下一个字符
peek-char的参数中port可以省略,默认情况下使用current-input-port的返回值。

以一个示例说明这三个过程的调用,示例实现从指定文件中按字符读取内容,并放到一个列表中。

(define input_port (open-input-file "temp"))
 
(define read-char-with-file (lambda (port)
(if (eof-object? (peek-char port))
'()
(cons (read-char port) (read-char-with-file port))
)
)
)
 
(if (input-port? input_port)
(begin
(display (read-char-with-file input_port))
(close-input-port input_port)
)
 
(begin
(display "read file form temp failed")
(newline)
)
)

这段代码写得有些复杂,我们可以通过let关键字和call-with-input-file过程来简化这段代码,如下:

(define output-chars
(call-with-input-file "temp"
(lambda (p)
(let f ((x (read-char p)))
(if (eof-object? x)
'()
(cons x (f (read-char p)))
) 
)
)
)
)
 
(display output-chars)
(newline)

call-with-input-file的标准调用格式为:

procedure: (call-with-input-file path procedure)

call-with-input-file过程给指定的文件创建一个port,并将这个port传递给第二个参数指定的过程procedure,当procedure执行完成时,call-with-input-file过程将关闭打开的输入port并返回procedure返回的值。

文件操作

上面的示例已经提了文件操作,关于文件操作,除了针对输入和输出的打开port和关闭port,判断port是否存在的过程外,我们常用还有判断文件存在,删除文件,Scheme提供的这两个过程来与文件系统交互:file-exists?和delete-file
file-exists?的作用是判断文件是否存在,其调用格式如下:
(file-exists? path)
如果文件存在,返回#t,否则返回#f

delete-file的作用是删除一个文件,其调用格式如下:
(delete-file path)

path是字符串类型,如果所给的文件不存在,则会显示错误:No such file or directory

在使用delete-file之前可以调用file-exists?来判断文件是否存在。

除了上面介绍的一些简单输入输出,Scheme还提供了更强大和灵活的输入输出机制,如对编码的处理,对二进制的处理,对字符串的操作等,具体可以见官方文档或《The Scheme Programming Language, 4th Edition》