International PHP Conference Berlin 2025

文件系统安全

目录

PHP 受大多数服务器系统中文件和目录权限的内置安全机制的影响。 这允许控制文件系统中哪些文件是可读的。应该小心对待任何全局可读的文件, 要确保所有有权限访问该文件系统的用户都可以安全的读取文件。

PHP 被设计为以用户级别访问文件系统, 因此完全可以编写 PHP 脚本来读取系统文件,例如 /etc/passwd、 修改网络连接、发送大量打印任务等。这有一些明显的影响,因此需要确保读写的是合适的文件。

请看下面的脚本,用户表示想要删除自己主目录下的一个文件。 假设 PHP web 界面通常用于文件管理, 因此 Apache 用户允许删除用户主目录中的文件。

示例 #1 不对变量检查会导致....

<?php

// 从用户主目录移除一个文件
$username = $_POST['user_submitted_name'];
$userfile = $_POST['user_submitted_filename'];
$homedir = "/home/$username";

unlink("$homedir/$userfile");

echo
"The file has been deleted!";

?>
由于 username 和 filename 由用户表单中提交,那就能提交属于其他人的 username 和 filename,甚至可以删除不被允许的文件。这种情况下, 应该使用一些其它形式的身份验证。不妨考虑一下,如果提交的变量是 “../etc/”“passwd” 会发生什么。上面代码将等同于:

示例 #2 ... 文件系统攻击

<?php

// 删除磁盘中任何 PHP 有访问权限的文件。如果 PHP 有 root 权限:
$username = $_POST['user_submitted_name']; // "../etc"
$userfile = $_POST['user_submitted_filename']; // "passwd"
$homedir = "/home/$username"; // "/home/../etc"

unlink("$homedir/$userfile"); // "/home/../etc/passwd"

echo "The file has been deleted!";

?>
有两个重要措施来防止此类问题。
  • PHP web 用户二进制文件仅允许有限的权限。
  • 检查所有提交上来的变量。
这是改进的脚本:

示例 #3 更安全的文件名检查

<?php

// 删除磁盘中 PHP 有权访问的文件。
$username = $_SERVER['REMOTE_USER']; // 使用认证机制
$userfile = basename($_POST['user_submitted_filename']);
$homedir = "/home/$username";

$filepath = "$homedir/$userfile";

if (
file_exists($filepath) && unlink($filepath)) {
$logstring = "Deleted $filepath\n";
} else {
$logstring = "Failed to delete $filepath\n";
}

$fp = fopen("/home/logging/filedelete.log", "a");
fwrite($fp, $logstring);
fclose($fp);

echo
htmlentities($logstring, ENT_QUOTES);

?>
然而,这样做也是有缺陷的。如果认证系统允许用户创建自己的登录用户名, 而用户选择 “../etc/” 作为登录名,系统将再次暴露。 出于这个原因,需要编写自定义检查:

示例 #4 更安全的文件名检查

<?php

$username
= $_SERVER['REMOTE_USER']; // 使用认证机制
$userfile = $_POST['user_submitted_filename'];
$homedir = "/home/$username";

$filepath = "$homedir/$userfile";

if (!
ctype_alnum($username) || !preg_match('/^(?:[a-z0-9_-]|\.(?!\.))+$/iD', $userfile)) {
die(
"Bad username/filename");
}

//等等...

?>

根据操作系统的不同,需要关心各种各样的文件,比如设备条目(/dev/COM1)、配置文件(/etc/ 文件和 .ini 文件)、 众所周知的文件存储区域(/home/My Documents)等等。出于这个原因, 创建一个禁止所有权限而只开放明确允许的策略通常更容易些。

添加备注

用户贡献的备注 5 notes

up
99
anonymous
19 years ago
(A) Better not to create files or folders with user-supplied names. If you do not validate enough, you can have trouble. Instead create files and folders with randomly generated names like fg3754jk3h and store the username and this file or folder name in a table named, say, user_objects. This will ensure that whatever the user may type, the command going to the shell will contain values from a specific set only and no mischief can be done.

(B) The same applies to commands executed based on an operation that the user chooses. Better not to allow any part of the user's input to go to the command that you will execute. Instead, keep a fixed set of commands and based on what the user has input, and run those only.

For example,
(A) Keep a table named, say, user_objects with values like:
username|chosen_name |actual_name|file_or_dir
--------|--------------|-----------|-----------
jdoe |trekphotos |m5fg767h67 |D
jdoe |notes.txt |nm4b6jh756 |F
tim1997 |_imp_ folder |45jkh64j56 |D

and always use the actual_name in the filesystem operations rather than the user supplied names.

(B)
<?php
$op
= $_POST['op'];//after a lot of validations
$dir = $_POST['dirname'];//after a lot of validations or maybe you can use technique (A)
switch($op){
case
"cd":
chdir($dir);
break;
case
"rd":
rmdir($dir);
break;
.....
default:
mail("webmaster@example.com", "Mischief", $_SERVER['REMOTE_ADDR']." is probably attempting an attack.");
}
up
25
fmrose at ncsu dot edu
19 years ago
All of the fixes here assume that it is necessary to allow the user to enter system sensitive information to begin with. The proper way to handle this would be to provide something like a numbered list of files to perform an unlink action on and then the chooses the matching number. There is no way for the user to specify a clever attack circumventing whatever pattern matching filename exclusion syntax that you may have.

Anytime you have a security issue, the proper behaviour is to deny all then allow specific instances, not allow all and restrict. For the simple reason that you may not think of every possible restriction.
up
22
devik at cdi dot cz
23 years ago
Well, the fact that all users run under the same UID is a big problem. Userspace security hacks (ala safe_mode) should not be substitution for proper kernel level security checks/accounting.
Good news: Apache 2 allows you to assign UIDs for different vhosts.
devik
up
10
Latchezar Tzvetkoff
15 years ago
A basic filename/directory/symlink checking may be done (and I personally do) via realpath() ...

<?php

if (isset($_GET['file'])) {
$base = '/home/polizei/public_html/'; // it seems this one is good to be realpath too.. meaning not a symlinked path..
if (strpos($file = realpath($base.$_GET['file']), $base) === 0 && is_file($file)) {
unlink($file);
} else {
die(
'blah!');
}
}
?>
up
6
cronos586(AT)caramail(DOT)com
22 years ago
when using Apache you might consider a apache_lookup_uri on the path, to discover the real path, regardless of any directory trickery.
then, look at the prefix, and compare with a list of allowed prefixes.
for example, my source.php for my website includes:
if(isset($doc)) {
$apacheres = apache_lookup_uri($doc);
$really = realpath($apacheres->filename);
if(substr($really, 0, strlen($DOCUMENT_ROOT)) == $DOCUMENT_ROOT) {
if(is_file($really)) {
show_source($really);
}
}
}
hope this helps
regards,
KAT44
To Top