虽然使用 finfo_file() 来验证上传的图像文件(检查一个假定的图像文件是否真的包含图像)可能看起来很诱人,但结果不可信。用一个被识别为 GIF 的文件来包装有害的可执行代码并不难。
一个更好更安全的选项是检查
if (!$img = @imagecreatefromgif($uploadedfilename)) {
trigger_error('不是 GIF 图像!',E_USER_WARNING);
// 执行必要的操作
}
(PHP >= 5.3.0, PHP 7, PHP 8, PECL fileinfo >= 0.1.0)
finfo_file -- finfo::file — 返回有关文件的信息
过程式风格
$finfo
,$filename
,$flags
= FILEINFO_NONE
,$context
= null
面向对象风格
$filename
, int $flags
= FILEINFO_NONE
, ?resource $context
= null
): string|false此函数用于获取有关文件的信息。
finfo
一个 finfo 实例,由 finfo_open() 返回。
filename
要检查的文件的名称。
flags
一个或多个 Fileinfo 常量 的析取。
context
有关 contexts
的描述,请参阅 流函数。
返回 filename
参数内容的文本描述,如果发生错误,则返回 false
。
示例 #1 一个 finfo_file() 示例
<?php
$finfo = finfo_open(FILEINFO_MIME_TYPE); // 返回 mime 类型,也称为 mimetype 扩展
foreach (glob("*") as $filename) {
echo finfo_file($finfo, $filename) . "\n";
}
finfo_close($finfo);
?>
上面的示例将输出类似于以下内容
text/html image/gif application/vnd.ms-excel
虽然使用 finfo_file() 来验证上传的图像文件(检查一个假定的图像文件是否真的包含图像)可能看起来很诱人,但结果不可信。用一个被识别为 GIF 的文件来包装有害的可执行代码并不难。
一个更好更安全的选项是检查
if (!$img = @imagecreatefromgif($uploadedfilename)) {
trigger_error('不是 GIF 图像!',E_USER_WARNING);
// 执行必要的操作
}
如何获取远程文件的 MIME 类型。
<?php
class MimeStreamWrapper
{
const WRAPPER_NAME = 'mime';
public $context;
private static $isRegistered = false;
private $callBackFunction;
private $eof = false;
private $fp;
private $path;
private $fileStat;
private function getStat()
{
if ($fStat = fstat($this->fp)) {
return $fStat;
}
$size = 100;
if ($headers = get_headers($this->path, true)) {
$head = array_change_key_case($headers, CASE_LOWER);
$size = (int)$head['content-length'];
}
$blocks = ceil($size / 512);
return array(
'dev' => 16777220,
'ino' => 15764,
'mode' => 33188,
'nlink' => 1,
'uid' => 10000,
'gid' => 80,
'rdev' => 0,
'size' => $size,
'atime' => 0,
'mtime' => 0,
'ctime' => 0,
'blksize' => 4096,
'blocks' => $blocks,
);
}
public function setPath($path)
{
$this->path = $path;
$this->fp = fopen($this->path, 'rb') or die('Cannot open file: ' . $this->path);
$this->fileStat = $this->getStat();
}
public function read($count) {
return fread($this->fp, $count);
}
public function getStreamPath()
{
return str_replace(array('ftp://', 'http://', 'https://'), self::WRAPPER_NAME . '://', $this->path);
}
public function getContext()
{
if (!self::$isRegistered) {
stream_wrapper_register(self::WRAPPER_NAME, get_class());
self::$isRegistered = true;
}
return stream_context_create(
array(
self::WRAPPER_NAME => array(
'cb' => array($this, 'read'),
'fileStat' => $this->fileStat,
)
)
);
}
public function stream_open($path, $mode, $options, &$opened_path)
{
if (!preg_match('/^r[bt]?$/', $mode) || !$this->context) {
return false;
}
$opt = stream_context_get_options($this->context);
if (!is_array($opt[self::WRAPPER_NAME]) ||
!isset($opt[self::WRAPPER_NAME]['cb']) ||
!is_callable($opt[self::WRAPPER_NAME]['cb'])
) {
return false;
}
$this->callBackFunction = $opt[self::WRAPPER_NAME]['cb'];
$this->fileStat = $opt[self::WRAPPER_NAME]['fileStat'];
return true;
}
public function stream_read($count)
{
if ($this->eof || !$count) {
return '';
}
if (($s = call_user_func($this->callBackFunction, $count)) == '') {
$this->eof = true;
}
return $s;
}
public function stream_eof()
{
return $this->eof;
}
public function stream_stat()
{
return $this->fileStat;
}
public function stream_cast($castAs)
{
$read = null;
$write = null;
$except = null;
return @stream_select($read, $write, $except, $castAs);
}
}
$path = 'http://fc04.deviantart.net/fs71/f/2010/227/4/6/PNG_Test_by_Destron23.png';
echo "File: ", $path, "\n";
$wrapper = new MimeStreamWrapper();
$wrapper->setPath($path);
$fInfo = new finfo(FILEINFO_MIME);
echo "MIME-type: ", $fInfo->file($wrapper->getStreamPath(), FILEINFO_MIME_TYPE, $wrapper->getContext()), "\n";
?>
只是注意到(因为我遇到了它!),当前的 finfo_file 实现有一个已知的错误,会导致 PHP 在检查文本文件时,如果文本文件中存在某些字符串,就会分配大量的内存。
有关更多信息,请参阅 https://bugs.php.net/bug.php?id=69224。
嗯,我遇到了一个大问题,MS Office 2007 扩展(pptx、xlsx、docx)没有默认的 MIME 类型,它们具有“application/zip” MIME 类型,因此,为了解决这个问题,我做了一个小函数来验证扩展名。
该函数允许你安全地避免虚假扩展名攻击。
<?php
$arrayZips = array("application/zip", "application/x-zip", "application/x-zip-compressed");
$arrayExtensions = array(".pptx", ".docx", ".dotx", ".xlsx");
$file = 'path/to/file.xlsx';
$original_extension = (false === $pos = strrpos($file, '.')) ? '' : substr($file, $pos);
$finfo = new finfo(FILEINFO_MIME);
$type = $finfo->file($file);
if (in_array($type, $arrayZips) && in_array($original_extension, $arrayExtensions))
{
return $original_extension;
}
?>
我之前一直得到 application/octet-stream 或者 "<= not supported" 的错误,对于所有文件都是这样。
我发现 PHP 5.3 中内建了 magic 文件,应该使用它。系统上找到的 magic 文件可能并不总是 libmagic 所期望的,因此会导致错误。
finfo_file 在 Windows 上的另一个有趣的特性。
对于某些文件类型(例如 ppt),此函数可能会返回空字符串而不是 FALSE。因此,为了确保,需要对输出结果进行三重检查,并在必要时提供默认类型。以下是一个示例代码
$ftype = 'application/octet-stream';
$finfo = @finfo_open(FILEINFO_MIME);
if ($finfo !== FALSE) {
$fres = @finfo_file($finfo, $file);
if ( ($fres !== FALSE)
&& is_string($fres)
&& (strlen($fres)>0)) {
$ftype = $fres;
}
@finfo_close($finfo);
}
我花了好几天时间寻找包含实际纯文本描述的媒体类型数据库,例如
finfo(.png) --> "image/png" --> "PNG image".
在基于 Ubuntu 的操作系统中,你可以在 /usr/share/mime 中找到已翻译的数据库
http://manpages.ubuntu.com/manpages/hardy/en/man5/gnome-mime.5.html
OO(略微改进)版本的相同内容
<?php
$file = '<somefile>';
$ftype = 'application/octet-stream';
$finfo = @new finfo(FILEINFO_MIME);
$fres = @$finfo->file($file);
if (is_string($fres) && !empty($fres)) {
$ftype = $fres;
}
?>
这是一个包装器,可以正确识别 Microsoft Office 2007 文档。它易于使用、编辑和添加更多文件扩展名/MIME 类型。
<?php
function get_mimetype($filepath) {
if(!preg_match('/\.[^\/\\\\]+$/',$filepath)) {
return finfo_file(finfo_open(FILEINFO_MIME_TYPE), $filepath);
}
switch(strtolower(preg_replace('/^.*\./','',$filepath))) {
// START MS Office 2007 Docs
case 'docx':
return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
case 'docm':
return 'application/vnd.ms-word.document.macroEnabled.12';
case 'dotx':
return 'application/vnd.openxmlformats-officedocument.wordprocessingml.template';
case 'dotm':
return 'application/vnd.ms-word.template.macroEnabled.12';
case 'xlsx':
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
case 'xlsm':
return 'application/vnd.ms-excel.sheet.macroEnabled.12';
case 'xltx':
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.template';
case 'xltm':
return 'application/vnd.ms-excel.template.macroEnabled.12';
case 'xlsb':
return 'application/vnd.ms-excel.sheet.binary.macroEnabled.12';
case 'xlam':
return 'application/vnd.ms-excel.addin.macroEnabled.12';
case 'pptx':
return 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
case 'pptm':
return 'application/vnd.ms-powerpoint.presentation.macroEnabled.12';
case 'ppsx':
return 'application/vnd.openxmlformats-officedocument.presentationml.slideshow';
case 'ppsm':
return 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12';
case 'potx':
return 'application/vnd.openxmlformats-officedocument.presentationml.template';
case 'potm':
return 'application/vnd.ms-powerpoint.template.macroEnabled.12';
case 'ppam':
return 'application/vnd.ms-powerpoint.addin.macroEnabled.12';
case 'sldx':
return 'application/vnd.openxmlformats-officedocument.presentationml.slide';
case 'sldm':
return 'application/vnd.ms-powerpoint.slide.macroEnabled.12';
case 'one':
return 'application/msonenote';
case 'onetoc2':
return 'application/msonenote';
case 'onetmp':
return 'application/msonenote';
case 'onepkg':
return 'application/msonenote';
case 'thmx':
return 'application/vnd.ms-officetheme';
//END MS Office 2007 Docs
}
return finfo_file(finfo_open(FILEINFO_MIME_TYPE), $filepath);
}
?>
我想使用 fileinfo 检查文件是否是 gzip 或 bzip2。但是,压缩文件的 MIME 类型是 "data",因为压缩是一种编码,而不是一种类型。
gzip 文件以二进制 1f8b 开头。
bzip2 文件以魔数字节 'B' 'Z' 'h' 开头。
例如:
<?php
$s = file_get_contents("somefilepath");
if ( bin2hex(substr($s,0,2)) == '1f8b' ) {/* could be a gzip file */}
if( substr($s,0,3) == 'BZh' ){/* could be a bzip2 file */}
?>
我不是编码专家。我唯一测试的是使用我自己编码的几个文件。
在使用这个新函数解决我的问题时,我突然想到使用文件的完整路径而不是相对路径。例如
<?php
$folder = "somefolder/";
$fileName "aFile.pdf";
$finfo = finfo_open(FILEINFO_MIME_TYPE);
finfo_file($finfo, $folder.$fileName);
?>
这会导致一个错误,它找不到指定的文件。
但是,这解决了这个问题
<?php
$folder = "somefolder/";
$fileName "aFile.pdf";
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, dirname(__FILE__)."/".$folder.$fileName);
?>
Ryan Day 发布的示例的改进版本 - 略微偏离主题,因为此方法没有使用 finfo_file,但在某些情况下,此方法可能更可取。
主要的更改是 `identify` 调用中传递了 `-format %m` 参数。我建议使用完整系统路径来识别,例如 `/usr/bin/identify`,这样更安全(不过,位置可能会因服务器而异)。
<?php
function is_jpg($fullpathtoimage){
if(file_exists($fullpathtoimage)){
exec("/usr/bin/identify -format %m $fullpathtoimage",$out);
// 使用 system() 自动输出 STDOUT
if(!empty($out)){
// 如果文件不是图像,identify 会返回一个空结果给 php
if($out == 'JPEG'){
return true;
}
}
}
return false;
}
?>