19.1.2 编写处理文件的PHP

编写捕获上载文件的PHP代码是相当直观和简单的。

当文件被上传时,该文件将保存在临时目录中,这是通过php.ini文件的upload_tmp_dir指令设置的。正如表19-1所介绍的,如果没有设置该指令,在默认情况下,该目录是Web服务器上的主临时目录。如果在脚本执行完成之前不移动、复制或更改文件名称,该文件将被删除。

在PHP脚本中,需要处理的数据保存在超级全局数组$_FILES中。如果开启了register_globals指令,也可以直接通过变量名称访问这些信息。但是,这可能就是关闭register_globals指令的原因,或者至少认为是关闭了该指令,并且使用超级全局数组而忽略全局变量。

保存$_FILES数组中的元素时,将同时保存HTML表单的<file>标记名称。表单元素名称是userfile,因此该数组将具有如下所示的内容:

■存储在$_FILES['userfile']['tmp_name']变量中的值就是文件在Web服务器中临时存储的位置。

■存储在$_FILES['userfile']['name']变量中的值就是用户系统中的文件名称。

■存储在$_FILES['userfile']['size']变量中的值就是文件的字节大小。

■存储在$_FILES['userfile']['type']变量中的值就是文件的MIME类型,例如:text/plain或image/gif。

■存储在$_FILES['userfile']['error']变量中的值将是任何与文件上传相关的错误代码。这是在PHP 4.2.0中增加的新特性。

如果已经知道上传文件的位置及其名称,现在,就可以将其复制到其他有用的地方。在脚本执行结束前,这个临时文件将被删除。因此,如果要保留上传文件,必须将其重命名或移动。

在我们的例子中,我们打算将上传文件作为最新的新闻文章,因此,我们将删除文件中可能的任何标记,再将它们移动到其他有用的目录,/uploads/目录。请注意,在Web服务器的根目录下,你需要创建一个名为uploads的目录。程序清单19-2给出了能够实现此功能的脚本。

程序清单19-2 upload.php——从HTML表单中获得文件的PHP脚本


<html>

<head>

<title>Uploading…</title>

</head>

<body>

<h1>Uploading file…</h1>

<?php

if($_FILES['userfile']['error']>0)

{

echo'Problem:';

switch($_FILES['userfile']['error'])

{

case 1:echo'File exceeded upload_max_filesize';

break;

case 2:echo'File exceeded max_file_size';

break;

case 3:echo'File only partially uploaded';

break;

case 4:echo'No file uploaded';

break;

case 6:echo'Cannot upload file:No temp directory specified';

break;

case 7:echo'Upload failed:Cannot write to disk';

break;

}

exit;

}

//Does the file have the right MIME type?

if($_FILES['userfile']['type']!='text/plain')

{

echo'Problem:file is not plain text';

exit;

}

//put the file where we'd like it

$upfile='/uploads/'.$_FILES['userfile']['name'];

if(is_uploaded_file($_FILES['userfile']['tmp_name']))

{

if(!move_uploaded_file($_FILES['userfile']['tmp_name'],$upfile))

{

echo'Problem:Could not move file to destination directory';

exit;

}

}

else

{

echo'Problem:Possible file upload attack.Filename:';

echo$_FILES['userfile']['name'];

exit;

}

echo'File uploaded successfully<br><br>';

//remove possible HTML and PHP tags from the file's contents

$contents=file_get_contents($upfile);

$contents=strip_tags($contents);

file_put_contents($_FILES['userfile']['name'],$contents);

//show what was uploaded

echo'<p>Preview of uploaded file contents:<br/><hr/>';

echo nl2br($contents);

echo'<br/><hr/>';

?>

</body>

</html>


有趣的是,以上脚本主要进行的操作是错误检测。文件上传存在潜在的安全危险,因此,我们应该尽量避免这些安全风险。我们还需要尽可能地测试文件以确认上传的文件是否适合显示给网站的访问者。

下面,我们来具体了解以上代码。首先,我们要检查在$_FILES['userfile']['error']中返回的错误代码。同样,每一个错误代码都有一个相关的错误常量。这些可能的常量和错误代码如下所示:

■UPLOAD_ERROR_OK,值为0,表示没有发生任何错误。

■UPLOAD_ERR_INI_SIZE,值为1,表示上传文件的大小超出了约定值。文件大小的最大值是在PHP配置文件中指定的,该指令是upload_max_filesize。

■UPLOAD_ERR_FORM_SIZE,值为2,表示上传文件大小超出了HTML表单的MAX_FILE_SIZE元素所指定的最大值。

■UPLOAD_ERR_PARTIAL,值为3,表示文件只被部分上传。

■UPLOAD_ERR_NO_FILE,值为4,表示没有上传任何文件。

■UPLOAD_NO_TMP_DIR,值为6,表示在php.ini文件中没有指定临时目录(在PHP 5.0.3版本引入)。

■UPLOAD_ERR_CANT_WRITE,值为7,表示将文件写入磁盘失败(在PHP 5.1.0版本引入)。

如果使用旧版本的PHP,也可以使用PHP手册给出的或本书以前版本给出的示例代码执行“手工版本”的检查操作。

也可以检查MIME类型。在这个例子中,我们只希望上传文本文件。因此通过确认$_FILES['userfile']['type']包含text/plain可以检测MIME类型。这只是错误检查。它不是安全性检查。MIME类型可以从用户浏览器判断文件扩展名获得并传递给服务器。如果希望通过传递一个错误的类型来“蒙混过关”,这对一些恶意的用户是很难的。

然后,我们再检查要打开的文件是否已经真正被上传而且不是一个本地文件(例如,/etc/passwd)。稍后,我们将详细介绍这一点。

如果所有的工作进展正常,我们可以将上传的文件复制到包含目录中。在这个例子中,我们使用/uploads/目录——它不存在于Web文档树结构中,因此是存放文件的适当位置,因为这些文件应该存放于Web文档树之外。

接下来,我们打开这个文件,使用strip_tags()函数清除任何HTML标记或PHP标记,并保存该文件。最后,显示文件的内容。这样,用户就可看到他们的文件被成功上传。

图19-2所示的是一个成功运行脚本的结果。

19.1.2 编写处理文件的PHP - 图1

图 19-2 在复制和重新格式化文件后,向用户显示成功上载文件的确认信息

在2000年9月,出现了一种软件,它能够让侵入者修改文件上传脚本,使此脚本可以将本地文件当成上传的文件进行处理。这个软件的文档保存在BUGTRAQ邮件发送清单中。

我们可以在许多BUGTRAQ文档中看到正式的安全咨询,例如:http://lists.insecure.org/bugtraq/2000/Sep/0237.html。

要确保自己不易受到攻击,这个脚本使用了is_uploaded_file()函数和move_uploaded_file()函数来确保所处理的文件已经被上传,而且不是一个本地文件,例如/etc/passwd。这个函数在PHP 4.0.3版本及其以后的版本都可以使用。

如果在编写文件上传脚本时考虑的问题不够全面,一些恶意的访问者可能会提供一个他自己的临时文件名称,并且使脚本认为这个文件就是上传的文件。由于许多文件上传脚本都会向用户显示所上传的文件内容,或者将其保存在可以载入该文件的地方,这就可能导致人们能够访问只有Web服务器才能访问的文件。这就可能包括一些敏感文件,例如,/etc/passwd和包括数据库密码的PHP源代码。

19.1.3 避免常见上传问题

在上传文件时,需要注意以下几个问题:

■前面的例子是假设用户已经在某些地方通过了身份验证。我们不应该允许任何人都可以上传文件到网站。

■如果允许不信任的或者没有通过身份验证的用户上传文件,就必须对文件的内容作出严格的规定。我们最不希望出现的是上传和运行恶意的脚本。应该非常小心,不仅是文件的类型和内容,还有文件本身的名字。一个好办法是将我们认为是“安全”的上传文件进行重命名。

■要降低用户“浏览Web服务器目录”的风险,你可以使用basename()函数来修改所接收文件的名称。这个函数将过滤作为文件名称一部分而传入的目录路径,这是在服务器的不同目录下放置文件的常见攻击手段。该函数示例如下所示:


<?php

$path="/home/httpd/html/index.php";

$file1=basename($path);

$file2=basename($path,".php");

print$file1."<br/>";//the value of$file1 is"index.php"

print$file2."<br/>";//the value of$file2 is"index"


■如果使用的是基于Windows的机器,通常情况下,必须确保在文件路径中用“\”或“/”替代“\”。

■就像我们以上脚本所做的,使用用户提供的文件名称可能会导致一系列的问题。最明显的问题就是如果上传的文件名称已经存在,可能会意外地覆盖已有文件。还有一个不是非常明显的风险是不同的操作系统,甚至是本地的不同语言设置将允许文件名称中包含不同的合法字符集。对于一个被上传的文件来说,其文件名称可能包含了对系统来说是非法的字符。

■如果文件上传脚本的运行出现问题,请检查PHP配置文件。需要将upload_tmp_dir指令设置成指向有访问权限的目录。如果要上传大文件,同时可能还需要调整memory_limit指令;该指令决定能够上传的最大文件字节数。Apache还有一些可配置的超时设置和事务大小限制,如果在上传大文件时遇到问题,可以考虑检查这些设置。