在文章应用安全系列之二十四:文件上传_jimmyleeee的博客-优快云博客描述了上传文件时需要判断文件的几个方面,其中一个就是文件的类型,当然,如果要根据文件的后缀判断,也能满足大部分需求,如果确实需要判断文件的类型,或者某些时刻文件没有扩展名时,就会遇到麻烦。
文件类型检测分为两种:
- 文件名检测 - 简单地根据文件后缀名判断文件类型。
- 魔术字检测 - 有些文件格式会将文件最开始的几个字节设置会特定的模式,通过这些特殊的字节模式,可以判断文件类型。
Java提供了集中方法来判断一个文件的类型,其中比较常用的方法是:Files.probeContentType,但是,它有一个缺点就是,当文件没有扩展名时,返回的结果是null,下面是一个使用这个方法的代码:
public class TestApp {
public static String identifyFileType(final String fileName) throws IOException
{
final File file = new File(fileName);
return Files.probeContentType(file.toPath());
}
public static void main(String[] args) {
String fileNames[] = {"F:\\filetype\\1.pdf",
"F:\\filetype\\2.zip",
"F:\\filetype\\3.docx",
"F:\\filetype\\4.xls",
"F:\\filetype\\5.jpg",
"F:\\filetype\\6.csv",
"F:\\filetype\\7.txt",
"F:\\filetype\\8.PNG",
"F:\\filetype\\9.mp4",
"F:\\filetype\\10.xml",
"F:\\filetype\\11.xml",
"F:\\filetype\\noextpng",
"F:\\filetype\\csvnoext",
"F:\\filetype\\mp4noext",
"F:\\filetype\\pngwithtxt.txt",
};
// Test Files.probeContentType
System.out.println("Test Files.probeContentType Begin");
try {
for (String fileName : fileNames) {
System.out.println(identifyFileType(fileName));
}
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("Test Files.probeContentType End");
}
}
打印结果如下:
Test Files.probeContentType Begin
application/pdf
application/x-zip-compressed
application/vnd.openxmlformats-officedocument.wordprocessingml.document
application/vnd.ms-excel
image/jpeg
application/vnd.ms-excel
text/plain
image/png
video/mp4
text/xml
application/json
null
null
null
text/plain
Test Files.probeContentType End
可以看到,Files.probeContentType是根据扩展名来判断文件类型的,没有扩展名时,返回的是null,当把一个图片改成txt结尾,返回的类型和txt的类型一样。说明如果要根据Files.probeContentType准确判断一个文件的类型是不准确的。
Java还提供了其他几个方法获取文件的类型:
类与方法名 | 说明 |
MimetypesFileTypeMap::getContentType | 只有jpg和txt返回正常,其他全部返回application/octet-stream |
URLConnection::getContentType | 和Files.probeContentType类似。不同的是,它识别的XML文件的类型是application/xml,而Files.probeContentType识别的XML是text/xml. |
URLConnection.guessContentTypeFromName | 测试了除了pdf、zip、jpg、txt、png、xml返回的值正常,其它全部返回null |
综上所述,目前Java提供的几种方法都不能很好地准确地判断文件的类型,不过,幸运的是Apache提供了Tika,下面是使用默认的Tika的功能的代码:
public static String getFileTypeByDefaultTika(final String fileName)
{
Tika defaultTika = new Tika();
String fileType;
try
{
final File file = new File(fileName);
fileType = defaultTika.detect(file);
}
catch (IOException ioEx)
{
fileType = "Unknown";
}
return fileType;
}
使用本文开头的示例代码打印信息如下:
Test DefaultTikaForFile Begin
application/pdf
application/zip
application/vnd.openxmlformats-officedocument.wordprocessingml.document
application/vnd.ms-excel
image/jpeg
text/csv
text/plain
image/png
video/mp4
application/xml
application/json
image/png
text/plain
video/quicktime
image/png
Test DefaultTikaForFile End
可以看到,所有的文件类型都识别出来了,没有扩展名的文件也是别出来,最为重要的是,我把图片改成txt后缀,它也是别出来这是一个png。
Tika还提供了其他实现,下面表格是将集中的实现结果的比较:
Tika实现方式 | 示例代码 | 说明 |
默认 | Tika defaultTika = new Tika(); | 根据上述代码打印结果,完全符合预期,效果最好; |
MIME | Tika mimeTika = new Tika(new MimeTypes()); | 除了txt文件正常输出text/plain,其他全部是application/octet-stream; |
TypeDetector | Tika typeTika = new Tika(new TypeDetector()); | 全部输出为application/octet-stream; |
综上所述,如果需要比较准确地判断一个文件到底是什么类型,不管扩展名是什么,需要使用Tika的默认实现方式;如果只需要根据扩展名判断文件类型就够了,你可以使用Files.probeContentType和URLConnection::getContentType。
关于PHP也有几种获取文件类型的方法,总结如下表:
类与方法名 | 说明 |
pathinfo ( $file , PATHINFO_EXTENSION) | 仅仅是根据文件的扩展名返回文件类型 |
$_FILES[‘uploadfile’][‘type’] | 仅仅是根据文件的扩展名返回文件类型 |
finfo_file( resource $finfo ,string $file_name = null ,int $options = FILEINFO_NONE,resource $context = null ): string | 本函数用来获取一个文件的信息。该方法即便是原文件被改过后缀,已然可以读到原文件类型。 |
总之,在处理文件上传时需要谨记需要做好输入验证,不管是文件名、大小、类型,还是内容等,只有做好了防范,才能预防由文件上传带来的攻击。