07/09/2018, 17:44

【PHP】Giá trị của $_FILES['userfile']['type'] có đáng tin cậy?

$FILES['userfile']['type'] là giá trị cho chúng ta biết kiểu file (kiểu MIME của file) . MIME type hiển thị tách nhau theo kiểu type/sub type. MINE type trong mỗi file được fix cố định FIle MIME type GIF image/jpeg JPEG image/jpeg PNG image/png HTML text/html ...

$FILES['userfile']['type'] là giá trị cho chúng ta biết kiểu file (kiểu MIME của file) .
MIME type hiển thị tách nhau theo kiểu type/sub type.
MINE type trong mỗi file được fix cố định

FIleMIME type
GIFimage/jpeg
JPEGimage/jpeg
PNGimage/png
HTMLtext/html
CSStext/css
JavaScripttext/javascript

MIME Type được quản lý bởi tổ chức có tên là Internet Assigned Numbers Authority(IANA)。Nó được đăng tải trên list Media Types 。
Giá trị của FILES['userfile']['type'] dựa trên loại file nhận được thông báo từ brower. Cụ thể nó dựa trên MIME type có trong message request. Thực tế, sử dụng Fidder, có thể kiểm tra được message request nó như thế nào。 Trong bài viết này chúng ta sẽ sử dụng GoogleChrome ở môi trường XAMPP của Windows 7 để kiểm tra。 Lựa chọn file, khi gửi , tạo page nhận hiển thị giá trị FILES['userfile']['type']

PHP
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>PHP</title>
</head>
<body>
<form  method="post" enctype="multipart/form-data">
    <p>Pictures:
        <input type="file" name="pictures">
        <input type="submit" value="送信">
    </p>
</form>
<?php
if(isset($FILES['pictures'])) {
    echo $FILES['pictures']['type'];
}
?>
</body>
</html>

Nếu là JPEG thì hiển thị là image/jpeg 、nếu là GIF thì hiển thị là image/gif、nếu là PNG thì hiển thị là image/png 。
Thực tế, chúng ta sẽ chọn file JPEG và thử gửi.
Theo như dự định thì nó sẽ hiển thị là image/jpeg 。
Khi đó , message request sẽ như bên dưới

HTTP
POST http://localhost/test/ HTTP/1.1
Host: localhost
Connection: keep-alive
Content-Length: 64201
Cache-Control: max-age=0
Origin: http://localhost
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7c22X5usFk6Z8VCU
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8
Referer: http://localhost/test/
Accept-Encoding: gzip, deflate, br
Accept-Language: ja,en-US;q=0.8,en;q=0.6

------WebKitFormBoundary7c22X5usFk6Z8VCU
Content-Disposition: form-data; name="pictures"; filename="01.jpg"
Content-Type: image/jpeg

Điều chú ý là Content-Type: image/jpeg của body mesage。
Nó hiển thị thông tin của MIME type 。
Giá trị của $FILES['userfile']['type'] được xác định dựa vào Content-Type: image/jpeg của message request này。
Khi nhìn kết quả này , sẽ thấy 1 điều đáng để quan tâm.
Brower xác định giá trị Content-Type: image/jpeg này như thế nào?
Khi mở text editor 、ghi script như bên dưới、gắn tên là xss.png , và save lại

xss.png
<script>alert("xss");</script>

Khi để kiểu mở rộng của nó là png nhưng được ghi vào bên trong tag script .
Chọn file này và thử gửi đi。Khi này phần content hiển thị thành image/png 。
Khi đó , message request sẽ như bên dưới

HTTP(
POST http://localhost/test/ HTTP/1.1
Host: localhost
Connection: keep-alive
Content-Length: 214
Cache-Control: max-age=0
Origin: http://localhost
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarySqOUDVmXAGnTyQYn
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8
Referer: http://localhost/test/
Accept-Encoding: gzip, deflate, br
Accept-Language: ja,en-US;q=0.8,en;q=0.6

------WebKitFormBoundarySqOUDVmXAGnTyQYn
Content-Disposition: form-data; name="pictures"; filename="xss.png"
Content-Type: image/png

<script>alert("xss");</script>
------WebKitFormBoundarySqOUDVmXAGnTyQYn--

Body message được ghi là Content-Type: image/png 。
Thay đổi tên file xss.png thành xss.jpg . Khi gửi thì body message của Content-Type lại là image/jpeg

Giá trị MIME type của message request được xác định dựa trên đuôi mở rộng , nên sẽ đưa ra kết quả không chính xác.

Về cơ bản, không chỉ giới hạn là Content-Type mà vì có khả năng toàn bộ message request có thể ngụy trang bởi thông tin khác nên giá trị được gửi đi bởi message request là hoàn toàn không chính xác.Do đó, vẫn cần check lại từ phía server (PHP)
Ở trên các browser khác Firefox、Opera cũng xảy ra tình trạng tương tự, khi kết hợp với đuôi mở rộng thì MIME type đã bị thay đổi

Trong trường hợp Internet Explorer 11 thì thế nào?

Khi lựa chọn xss.jpg, gửi đi bằng Internet Explorer 11
Request message như bên dưới。

HTTP
POST http://localhost/test/ HTTP/1.1
Accept: text/html, application/xhtml+xml, /
Referer: http://localhost/test/
Accept-Language: ja-JP
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko
Content-Type: multipart/form-data; boundary=---------------------------7e125bc25117e
Accept-Encoding: gzip, deflate
Connection: Keep-Alive
Content-Length: 245
DNT: 1
Host: localhost
Pragma: no-cache

-----------------------------7e125bc25117e
Content-Disposition: form-data; name="pictures"; filename="C:Users7968Desktopxss.jpg"
Content-Type: text/plain

<script>alert("xss");</script>
-----------------------------7e125bc25117e--

Content-Type trong body message text/plain 。
Dù có thay đổi kiểu mở rộng thành xss.png thì giá trị content- Type vẫn là text/plain 。
Vì từ bên trong file Internet、có chức năng xác định MIME type .
Có khả năng là chức năng này hoạt động và phán đoán ra Content- Type là text/plain
Trong trường hợp Internet Explorer , MIME type khác với browser khác nhưng mà dù thế nào vẫn cần check giá trị này ở phía server(PHP)
Vậy liệu có phương pháp nào để xác định chính xác loại file từ phía PHP?

Phương pháp check type file được upload từ PHP

1.Phương pháp check data binary của file
Trong data binary sẽ chứa các thông tin để xác định loại file .Nó có thể được gọi bằng rất nhiều tên
Magic number, format identifier,magic Byte...
Magic number là dãy kí tự đầu tiên của file.Dựa vào dãy kí tự này kết hợp với get thông tin data binary , ta có thể xác định loại file

TypeMagic number(Hex)
png89 50 4E 47
jpegFF D8 DD E0
jpgFF D8 FF EE
GIF(89a)47 49 46 38 39 61
GIF(87a)47 49 46 38 37 61

Chúng ta có thể get data binary của file thông qua đoạn code bên dưới

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>PHP</title>
</head>
<body>
<form method="post" enctype="multipart/form-data">
    <p>Pictures:
        <input type="file" name="pictures">
        <input type="submit" value="送信">
    </p>
</form>
<?php
if(isset($_FILES['pictures'])) {
    $tmp_file_data = bin2hex(file_get_contents($_FILES['pictures']['tmp_name']));
    echo $tmp_file_data;
}
?>
</body>
</html>

2.Phương pháp check MIME type của file
Thay vì check MIME type của từ message request như bên trên , chúng ta có thể xác định file dựa vào một số function or class bên dưới

function/classPhP mannul
mime_content_typePHP 4 >= 4.3.0, PHP 5, PHP 7
Fileinfo FunctionPHP 5 >= 5.3.0, PHP 7
class finfoPHP 5 >= 5.3.0, PHP 7
getimagesizePHP 4, PHP 5, PHP 7
exif-imagetype PHP 4 >= 4.3.0, PHP 5, PHP 7
0