آپلود فایل یکی از قابلیتهای رایج در بسیاری از وبسایتها و برنامههای PHP است. تقریباً هر سیستم واقعی ممکن است به آپلود تصویر پروفایل، تصویر محصول، فایل PDF، مدارک، فایلهای پیوست یا فایلهای مربوط به محتوا نیاز داشته باشد.
اما آپلود فایل فقط دریافت یک فایل و ذخیره کردن آن روی سرور نیست. اگر فرآیند آپلود بدون اعتبارسنجی و کنترلهای امنیتی مناسب پیادهسازی شود، مهاجم میتواند فایلهای خطرناک را وارد سیستم کند و در شرایط خاص حتی باعث اجرای کد روی سرور شود.
به همین دلیل، امنیت آپلود فایل در PHP یکی از موضوعات مهم امنیت برنامههای وب است.
در این مقاله یاد میگیریم چگونه یک سیستم آپلود فایل امن در PHP ایجاد کنیم و با استفاده از بررسی نوع فایل، MIME Type، پسوند، اندازه، محتوای واقعی فایل، نام تصادفی و محل ذخیرهسازی، ریسک File Upload Vulnerability را کاهش دهیم.
File Upload Vulnerability چیست؟
File Upload Vulnerability یا آسیبپذیری آپلود فایل زمانی رخ میدهد که برنامه وب اجازه دهد کاربر فایلهایی را روی سرور قرار دهد، اما فایل به اندازه کافی اعتبارسنجی و کنترل نشود.
فرض کنید یک فرم ساده برای آپلود فایل داریم:
<form method="POST" enctype="multipart/form-data">
<input type="file" name="file">
<button type="submit">آپلود</button>
</form>
اگر برنامه سمت سرور بدون بررسی فایل را ذخیره کند:
move_uploaded_file(
$_FILES['file']['tmp_name'],
'uploads/' . $_FILES['file']['name']
);
چند مشکل جدی ایجاد میشود.
برای مثال:
- کاربر میتواند فایل با پسوند نامناسب ارسال کند.
- نام فایل میتواند حاوی کاراکترهای غیرمنتظره باشد.
- اندازه فایل میتواند بسیار بزرگ باشد.
- MIME Type اعلامشده توسط مرورگر قابل اعتماد نیست.
- ممکن است فایل واقعاً تصویری نباشد.
- اگر فایل PHP در یک مسیر قابل اجرا قرار بگیرد، احتمال اجرای کد وجود دارد.
- فایلهای حساس ممکن است در دسترس کاربران غیرمجاز قرار بگیرند.
بنابراین نباید فقط به این موضوع فکر کنیم که:
> «آیا فایل با موفقیت آپلود شد؟»
بلکه باید بررسی کنیم:
> «آیا این فایل واقعاً همان نوع فایلی است که برنامه اجازه آپلود آن را داده است؟»
# فرآیند امن آپلود فایل چگونه است؟
یک فرآیند مناسب برای آپلود فایل معمولاً شامل این مراحل است:
دریافت فایل
↓
بررسی خطای آپلود
↓
بررسی اندازه فایل
↓
تشخیص MIME واقعی
↓
بررسی پسوند
↓
بررسی محتوای فایل
↓
تولید نام تصادفی
↓
انتخاب محل ذخیرهسازی امن
↓
move_uploaded_file()
↓
ثبت اطلاعات فایل
هر کدام از این مراحل نقش متفاوتی در امنیت سیستم دارند.
# فرم HTML صحیح برای آپلود فایل در PHP
برای ارسال فایل با فرم HTML باید از:
enctype="multipart/form-data"
استفاده کنیم.
یک فرم ساده:
<form action="upload.php" method="POST" enctype="multipart/form-data">
<label for="file">انتخاب فایل:</label>
<input
type="file"
id="file"
name="file"
accept=".jpg,.jpeg,.png,.webp"
>
<button type="submit">
آپلود فایل
</button>
</form>
نکته مهم این است که accept یک قابلیت سمت مرورگر است و مکانیزم امنیتی محسوب نمیشود.
برای مثال:
accept=".jpg,.jpeg,.png"
به این معنی نیست که کاربر حتماً یک فایل JPG یا PNG واقعی ارسال خواهد کرد.
اعتبارسنجی اصلی باید در PHP انجام شود.
# اطلاعات فایل در $_FILES
بعد از ارسال فایل، PHP اطلاعات آن را در آرایه $_FILES قرار میدهد.
مثلاً:
var_dump($_FILES['file']);
ممکن است خروجی شبیه این باشد:
[
"name" => "photo.jpg",
"full_path" => "photo.jpg",
"type" => "image/jpeg",
"tmp_name" => "/tmp/phpABC123",
"error" => 0,
"size" => 24576
]
مهمترین مقادیر عبارتاند از:
name
نام اصلی فایل ارسالشده توسط کاربر است.
مثلاً:
photo.jpg
نباید این مقدار را بدون بررسی به عنوان نام فایل نهایی استفاده کنیم.
type
نوع MIME اعلامشده توسط کلاینت است.
مثلاً:
image/jpeg
اما این مقدار را نباید به عنوان یک منبع قابل اعتماد برای امنیت در نظر بگیریم.
tmp_name
مسیر فایل موقت روی سرور است.
مثلاً:
/tmp/phpABC123
این فایل معمولاً توسط PHP در زمان پردازش آپلود ایجاد میشود.
error
وضعیت آپلود فایل را مشخص میکند.
برای مثال:
UPLOAD_ERR_OK
یعنی آپلود بدون خطا انجام شده است.
size
اندازه فایل بر حسب بایت است.
مثلاً:
24576
یعنی حدود 24 کیلوبایت.
# بررسی خطای آپلود
قبل از هر کار بهتر است وضعیت آپلود را بررسی کنیم:
if (!isset($_FILES['file'])) {
exit('فایلی ارسال نشده است.');
}
if ($_FILES['file']['error'] !== UPLOAD_ERR_OK) {
exit('آپلود فایل با خطا مواجه شد.');
}
در پروژههای واقعی بهتر است خطاهای مختلف را نیز جداگانه مدیریت کنیم.
برای مثال:
switch ($_FILES['file']['error']) {
case UPLOAD_ERR_OK:
break;
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
exit('حجم فایل بیش از حد مجاز است.');
case UPLOAD_ERR_NO_FILE:
exit('لطفاً یک فایل انتخاب کنید.');
default:
exit('خطایی در آپلود فایل رخ داده است.');
}
# محدود کردن اندازه فایل
یکی از کنترلهای مهم، محدود کردن حجم فایل است.
فرض کنیم حداکثر حجم مجاز ما 2 مگابایت است:
$maxSize = 2 * 1024 * 1024;
if ($_FILES['file']['size'] > $maxSize) {
exit('حجم فایل نباید بیشتر از 2 مگابایت باشد.');
}
این بررسی را باید در سمت سرور انجام دهیم.
تنظیمات PHP نیز روی آپلود تأثیر دارند.
برای مثال:
upload_max_filesize = 2M
post_max_size = 2M
اما این تنظیمات جایگزین اعتبارسنجی داخل برنامه نیستند.
بهتر است برنامه نیز محدودیت مورد نیاز خودش را بررسی کند.
# چرا نباید به $_FILES['type'] اعتماد کنیم؟
یکی از اشتباهات رایج این است:
if ($_FILES['file']['type'] === 'image/jpeg') {
// فایل معتبر است
}
این روش به تنهایی مناسب نیست.
دلیل آن این است که اطلاعات ارسالشده از سمت کلاینت را نمیتوان مرجع نهایی تشخیص نوع واقعی فایل قرار داد.
مثلاً ممکن است فایل ظاهراً:
image.jpg
باشد، اما محتوای واقعی آن چیز دیگری باشد.
بنابراین بهتر است MIME Type فایل را روی سرور تشخیص دهیم.
# تشخیص MIME Type واقعی با finfo_file
PHP ابزار مناسبی برای تشخیص MIME Type فایل دارد:
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($_FILES['file']['tmp_name']);
echo $mimeType;
برای یک تصویر JPEG ممکن است نتیجه:
image/jpeg
باشد.
برای PNG:
image/png
و برای WebP:
image/webp
این روش از بررسی ساده مقدار $_FILES['type'] قابل اتکاتر است.
# استفاده از Whitelist برای MIME Type
به جای اینکه بگوییم:
> هر فایلی به جز چند فایل ممنوع را قبول کن
بهتر است بگوییم:
> فقط این انواع فایل مجاز هستند.
مثلاً:
$allowedMimeTypes = [
'image/jpeg',
'image/png',
'image/webp',
];
سپس:
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($_FILES['file']['tmp_name']);
if (!in_array($mimeType, $allowedMimeTypes, true)) {
exit('نوع فایل مجاز نیست.');
}
این روش را Whitelist مینامیم.
# بررسی پسوند فایل
MIME Type مهم است، اما بهتر است پسوند فایل نیز بررسی شود.
برای دریافت پسوند:
$extension = strtolower(
pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION)
);
سپس:
$allowedExtensions = [
'jpg',
'jpeg',
'png',
'webp',
];
if (!in_array($extension, $allowedExtensions, true)) {
exit('پسوند فایل مجاز نیست.');
}
اما یک نکته مهم وجود دارد:
پسوند به تنهایی معیار امنیتی مناسبی نیست.
مثلاً تغییر نام:
malicious.php
به:
image.jpg
محتوای واقعی فایل را تغییر نمیدهد.
بنابراین بهتر است:
پسوند + MIME Type + محتوای فایل
را در کنار هم بررسی کنیم.
# چرا باید نام فایل را تغییر دهیم؟
فرض کنید کاربر فایلی با نام زیر آپلود کند:
my-photo.jpg
و ما دقیقاً همان نام را ذخیره کنیم:
move_uploaded_file(
$_FILES['file']['tmp_name'],
'uploads/' . $_FILES['file']['name']
);
این کار مشکلات مختلفی دارد.
برای مثال:
- احتمال برخورد نام فایلها وجود دارد.
- نام فایل تحت کنترل کاربر است.
- نام میتواند شامل کاراکترهای نامناسب باشد.
- ممکن است مدیریت مسیر فایل دشوار شود.
- در بعضی شرایط میتواند ریسک Path Traversal ایجاد کند.
به همین دلیل بهتر است نام فایل را خود برنامه تولید کند.
# تولید نام تصادفی برای فایل
یکی از روشهای مناسب:
$randomName = bin2hex(random_bytes(16));
مثلاً خروجی:
8f3d1a5c9b7e21d4c6a8f0b2e3d4a5c6
سپس پسوند معتبر را به آن اضافه میکنیم:
$newFileName = $randomName . '.' . $extension;
مثلاً:
8f3d1a5c9b7e21d4c6a8f0b2e3d4a5c6.jpg
به این ترتیب نام اصلی فایل کاربر دیگر مستقیماً در مسیر ذخیرهسازی استفاده نمیشود.
# آیا استفاده از basename() کافی است؟
ممکن است چنین کدی را ببینید:
$fileName = basename($_FILES['file']['name']);
استفاده از basename() میتواند بعضی مشکلات مربوط به مسیر را کاهش دهد، اما به تنهایی یک راهکار کامل امنیتی نیست.
حتی بعد از استفاده از:
basename()
هنوز باید موارد زیر بررسی شوند:
- نوع واقعی فایل
- پسوند
- حجم
- محتوای فایل
- محل ذخیرهسازی
- نام فایل
- سطح دسترسی
در سیستمهای حساس، بهترین رویکرد این است که نام اصلی کاربر را برای نام فایل ذخیرهشده استفاده نکنیم.
# ذخیره فایل در مسیر خارج از Web Root
یکی از مهمترین تصمیمهای معماری در سیستم آپلود فایل، محل ذخیره فایل است.
فرض کنید ساختار سرور ما این باشد:
/var/www/project/
├── public/
│ ├── index.php
│ └── uploads/
└── storage/
اگر فایلهای حساس را داخل:
public/uploads/
ذخیره کنیم، ممکن است مستقیماً از طریق URL قابل دسترسی باشند.
برای فایلهای خصوصی بهتر است فایل را خارج از Web Root ذخیره کنیم:
/var/www/project/storage/uploads/
سپس در صورت نیاز، فایل را از طریق PHP و بعد از بررسی دسترسی کاربر ارائه کنیم.
# فایلهای خصوصی را مستقیم از URL ارائه نکنید
فرض کنید فایل کاربران در:
storage/private/
قرار دارد.
به جای اینکه URL مستقیم فایل را در اختیار کاربر قرار دهیم، میتوانیم یک endpoint داشته باشیم:
download.php?id=123
سپس PHP:
- کاربر را احراز هویت میکند.
- بررسی میکند اجازه دسترسی به فایل را دارد یا خیر.
- فایل مورد نظر را پیدا میکند.
- آن را به کاربر ارسال میکند.
این روش برای فایلهای خصوصی مثل:
- مدارک
- فاکتورها
- فایلهای خریداریشده
- فایلهای کاربران
- فایلهای آموزشی خصوصی
مناسبتر است.
# استفاده از move_uploaded_file()
برای انتقال فایل آپلودشده میتوانیم از:
move_uploaded_file()
استفاده کنیم.
مثال:
$source = $_FILES['file']['tmp_name'];
$destination = __DIR__ . '/uploads/' . $newFileName;
if (!move_uploaded_file($source, $destination)) {
exit('ذخیره فایل با خطا مواجه شد.');
}
این تابع برای انتقال فایلهایی که از طریق HTTP POST آپلود شدهاند طراحی شده است.
# بررسی واقعی بودن تصویر با getimagesize()
اگر سیستم فقط تصویر قبول میکند، بهتر است یک بررسی مخصوص تصویر نیز انجام دهیم.
مثلاً:
$imageInfo = getimagesize($_FILES['file']['tmp_name']);
if ($imageInfo === false) {
exit('فایل تصویر معتبر نیست.');
}
این بررسی کمک میکند فایلهایی که صرفاً پسوند تصویری دارند اما تصویر واقعی نیستند، راحتتر شناسایی شوند.
برای مثال:
file.php
اگر فقط نام آن به:
file.jpg
تغییر کند، تبدیل به یک تصویر واقعی نمیشود.
# بررسی MIME و محتوای تصویر در کنار هم
برای آپلود تصاویر میتوانیم از ترکیب این دو روش استفاده کنیم:
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($_FILES['file']['tmp_name']);
$allowedMimeTypes = [
'image/jpeg',
'image/png',
'image/webp',
];
if (!in_array($mimeType, $allowedMimeTypes, true)) {
exit('نوع تصویر مجاز نیست.');
}
if (getimagesize($_FILES['file']['tmp_name']) === false) {
exit('فایل تصویر معتبر نیست.');
}
این روش از تکیه بر نام فایل بسیار امنتر است.
# آیا SVG را میتوان مثل JPG و PNG آپلود کرد؟
خیر، باید با SVG با احتیاط بیشتری برخورد کرد.
SVG یک فرمت متنی مبتنی بر XML است و میتواند قابلیتهایی داشته باشد که آن را از یک تصویر ساده متفاوت میکند.
به همین دلیل اگر سایت شما واقعاً به SVG نیاز ندارد، سادهترین تصمیم امنیتی میتواند این باشد که آن را اصلاً قبول نکند.
اگر نیاز به SVG دارید، باید آن را با یک فرآیند Sanitization مناسب پاکسازی کنید و صرفاً بر اساس:
image/svg+xml
آن را معتبر فرض نکنید.
# آیا PDF کاملاً امن است؟
PDF نیز صرفاً یک تصویر ساده نیست.
فایلهای PDF میتوانند قابلیتهای پیچیده مختلفی داشته باشند.
بنابراین اگر برنامه شما به PDF نیاز دارد، بهتر است:
- حجم فایل را محدود کنید.
- MIME Type را بررسی کنید.
- پسوند را بررسی کنید.
- فایل را خارج از Web Root ذخیره کنید، مخصوصاً اگر خصوصی است.
- در سیستمهای حساس اسکن امنیتی فایل را در نظر بگیرید.
- فایل را مستقیماً به عنوان محتوای HTML یا اسکریپت پردازش نکنید.
# چرا نباید فایل PHP را در پوشه آپلود عمومی قرار دهیم؟
یکی از مهمترین خطرات، قرار گرفتن فایلهای اجرایی در مسیری است که وبسرور میتواند آنها را اجرا کند.
فرض کنید:
public/uploads/
یک پوشه عمومی باشد.
اگر سیستم اجازه دهد فایل PHP در این مسیر قرار گیرد و وبسرور آن را به عنوان PHP اجرا کند، مشکل امنیتی بسیار جدی ایجاد میشود.
بنابراین اگر برنامه شما فقط تصویر و PDF نیاز دارد، اصلاً اجازه آپلود:
.php
.phtml
.php3
.php4
.php5
و فرمتهای اجرایی مشابه را ندهید.
اما توجه کنید که فقط Blacklist کردن چند پسوند کافی نیست.
راهکار اصلی، طراحی صحیح فرآیند آپلود و جلوگیری از اجرای فایلهای آپلودشده است.
# جلوگیری از اجرای فایل در Upload Directory
اگر فایلهای آپلودی باید در یک مسیر عمومی قرار بگیرند، باید تنظیمات Web Server به شکلی باشد که فایلهای آپلودشده به عنوان کد سمت سرور اجرا نشوند.
این موضوع بسته به وبسرور متفاوت است.
برای مثال، اگر از Apache، Nginx یا OpenLiteSpeed استفاده میکنید، باید تنظیمات مربوط به اجرای فایلهای PHP در مسیر آپلود را بررسی کنید.
اصل مهم این است:
Uploaded File
↓
Storage
↓
Never Execute
حتی اگر فایل از نظر نام و MIME Type بررسی شده باشد، جلوگیری از اجرای فایل آپلودشده یک لایه دفاعی مهم دیگر است.
# از دسترسی 777 برای پوشه آپلود استفاده نکنید
گاهی برای حل مشکل Permission، توسعهدهنده پوشه را به:
777
تغییر میدهد.
این کار راهکار مناسبی برای محیط Production نیست.
بهتر است مالکیت و Permission فایلها و پوشهها متناسب با کاربر اجرای وبسرور تنظیم شود.
اصل مهم:
> فقط حداقل دسترسی مورد نیاز را بدهید.
این همان اصل Least Privilege است.
# محدود کردن تعداد فایلهای آپلودشده
فقط اندازه هر فایل مهم نیست.
فرض کنید هر فایل حداکثر:
2 MB
باشد.
اگر یک کاربر بتواند هزاران فایل 2 مگابایتی آپلود کند، باز هم ممکن است فضای دیسک سرور مصرف شود.
بنابراین در سیستمهای واقعی بهتر است موارد زیر نیز کنترل شوند:
- تعداد آپلود در دقیقه
- تعداد آپلود در ساعت
- تعداد فایلهای هر کاربر
- فضای مصرفی هر کاربر
- تعداد درخواستهای آپلود
برای سیستمهای عمومی، Rate Limiting نیز میتواند مفید باشد.
# جلوگیری از Path Traversal در آپلود فایل
نباید مسیر ذخیرهسازی را مستقیماً از ورودی کاربر بسازیم.
مثلاً چنین چیزی خطرناک است:
$path = $_POST['path'];
move_uploaded_file(
$_FILES['file']['tmp_name'],
$path
);
در این حالت کاربر عملاً روی محل ذخیره فایل تأثیر زیادی دارد.
بهتر است مسیر را برنامه تعیین کند:
$uploadDirectory = __DIR__ . '/uploads/';
و نام فایل نیز توسط برنامه تولید شود:
$newFileName = bin2hex(random_bytes(16)) . '.' . $extension;
در نتیجه کاربر فقط فایل را انتخاب میکند و کنترل مسیر و نام نهایی در اختیار برنامه است.
# نام فایل اصلی را کجا نگه داریم؟
اگر لازم است نام اصلی فایل را برای نمایش به کاربر حفظ کنیم، بهتر است آن را جدا از نام فایل فیزیکی ذخیره کنیم.
مثلاً در دیتابیس:
id: 15
original_name: product-image.jpg
stored_name: 8f3d1a5c9b7e21d4.jpg
mime_type: image/jpeg
size: 245760
path: uploads/8f3d1a5c9b7e21d4.jpg
به این ترتیب:
original_name
برای نمایش به کاربر است و:
stored_name
برای مدیریت فایل روی سرور.
# یک نمونه کامل آپلود امن تصویر در PHP
حالا یک مثال کامل ایجاد کنیم.
فرض کنیم فقط این فرمتها مجاز هستند:
JPG
PNG
WebP
و حداکثر حجم:
2 MB
باشد.
کد:
<?php
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
exit('درخواست نامعتبر است.');
}
if (!isset($_FILES['file'])) {
exit('فایلی انتخاب نشده است.');
}
$file = $_FILES['file'];
if ($file['error'] !== UPLOAD_ERR_OK) {
exit('آپلود فایل با خطا مواجه شد.');
}
$maxSize = 2 * 1024 * 1024;
if ($file['size'] > $maxSize) {
exit('حجم فایل نباید بیشتر از 2 مگابایت باشد.');
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
$allowedTypes = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
];
if (!isset($allowedTypes[$mimeType])) {
exit('نوع فایل مجاز نیست.');
}
if (getimagesize($file['tmp_name']) === false) {
exit('فایل تصویر معتبر نیست.');
}
$extension = $allowedTypes[$mimeType];
$newFileName = bin2hex(random_bytes(16)) . '.' . $extension;
$uploadDirectory = __DIR__ . '/uploads/';
if (!is_dir($uploadDirectory)) {
mkdir($uploadDirectory, 0755, true);
}
$destination = $uploadDirectory . $newFileName;
if (!move_uploaded_file($file['tmp_name'], $destination)) {
exit('ذخیره فایل با خطا مواجه شد.');
}
echo 'فایل با موفقیت آپلود شد.';
در این مثال چند لایه امنیتی داریم:
HTTP Method
↓
File Exists
↓
Upload Error
↓
File Size
↓
Real MIME Type
↓
Allowed MIME Types
↓
Image Validation
↓
Random Filename
↓
Controlled Directory
↓
move_uploaded_file()
# چرا در این کد از نام اصلی فایل استفاده نکردیم؟
فرض کنید کاربر فایل زیر را انتخاب کند:
عکس محصول جدید.jpg
ما این نام را مستقیماً ذخیره نمیکنیم.
در عوض برنامه نامی مثل:
6b7f9c2e1a4d8f0c3b5e7a9d2c6f1e8a.jpg
ایجاد میکند.
مزایای این کار:
- جلوگیری از Collision
- کاهش وابستگی به نام کاربر
- مدیریت سادهتر فایل
- جلوگیری از بسیاری از مشکلات نام فایل
- پیشبینیناپذیر شدن نام فایل
# آیا تغییر پسوند فایل باعث امن شدن آن میشود؟
خیر.
این تصور اشتباه است:
file.php
↓ rename
file.jpg
تغییر نام فایل به تنهایی محتوای آن را تغییر نمیدهد.
بنابراین:
$fileName = 'image.jpg';
به این معنی نیست که فایل واقعاً تصویر است.
برای تشخیص نوع واقعی فایل باید محتوای آن بررسی شود.
# Re-Encode کردن تصاویر برای امنیت بیشتر
در بعضی سیستمهای حساس، علاوه بر بررسی MIME و محتوای تصویر، میتوان تصویر را مجدداً Decode و Encode کرد.
برای مثال، تصویر دریافت میشود:
original image
↓
Decode
↓
Create new image
↓
Encode
↓
new clean image
با استفاده از کتابخانههایی مانند GD یا Imagick میتوان تصویر را مجدداً تولید کرد.
برای نمونه، در شرایط مناسب میتوان یک JPEG را خواند:
$image = imagecreatefromjpeg($file['tmp_name']);
و سپس یک فایل جدید تولید کرد:
imagejpeg($image, $destination, 90);
این روش میتواند برای پاکسازی برخی دادههای اضافی تصویر مفید باشد، اما خودش جایگزین اعتبارسنجی MIME، محدودیت حجم و سایر کنترلهای امنیتی نیست.
# امنیت فایل ZIP و Archive
اگر سیستم اجازه آپلود ZIP یا فایلهای فشرده را میدهد، موضوع پیچیدهتر میشود.
برای مثال، هنگام Extract کردن یک ZIP نباید صرفاً فایلها را در مسیر مقصد استخراج کنیم.
فایلهای داخل Archive ممکن است مسیرهایی مانند:
../../some-file
داشته باشند.
این مسئله میتواند به Path Traversal یا اصطلاحاً Zip Slip منجر شود.
بنابراین اگر فایلهای ZIP را Extract میکنید باید:
- مسیر فایلها را بررسی کنید.
- از خروج فایلها از مسیر مقصد جلوگیری کنید.
- Symlinkها را با دقت مدیریت کنید.
- نوع فایلهای داخل Archive را بررسی کنید.
- محدودیت حجم و تعداد فایل داشته باشید.
اگر نیازی به ZIP ندارید، قبول نکردن آن سادهترین راه کاهش ریسک است.
# اسکن آنتیویروس فایلهای آپلودشده
برای سیستمهای حساس، میتوان یک لایه اسکن امنیتی نیز اضافه کرد.
مثلاً:
Upload
↓
Validation
↓
Virus/Malware Scan
↓
Safe Storage
این روش مخصوصاً برای سیستمهایی که کاربران میتوانند فایلهای مختلفی مانند:
- ZIP
- Office Documents
- فایلهای ناشناخته
ارسال کنند، میتواند مفید باشد.
البته اسکن آنتیویروس نیز نباید جایگزین طراحی امن سیستم شود.
# امنیت فایلهای دانلودی
امنیت فقط مربوط به مرحله Upload نیست.
اگر فایلها بعداً توسط کاربران دانلود میشوند، باید مرحله Download نیز کنترل شود.
برای مثال:
download.php?id=15
نباید اجازه دهد کاربر مستقیماً مسیر دلخواه خودش را ارسال کند.
بد:
$file = $_GET['file'];
readfile($file);
بهتر است:
File ID
↓
Database
↓
Find File
↓
Check User Permission
↓
Resolve Server Path
↓
Read File
یعنی کاربر فقط یک شناسه منطقی فایل را ارائه کند و مسیر واقعی فایل توسط برنامه از دیتابیس مشخص شود.
# استفاده از X-Content-Type-Options
برای پاسخهایی که فایلها را ارائه میکنند، یکی از Headerهای مفید:
X-Content-Type-Options: nosniff
است.
در PHP میتوان آن را با:
header('X-Content-Type-Options: nosniff');
تنظیم کرد.
این Header به مرورگر اعلام میکند که نباید نوع محتوا را به شکل دلخواه خودش حدس بزند.
# استفاده از Content-Disposition برای فایلهای دانلودی
اگر میخواهیم فایل به عنوان دانلود ارائه شود، میتوانیم از:
header('Content-Disposition: attachment; filename="file.pdf"');
استفاده کنیم.
البته نام فایل در Header نیز باید به شکل صحیح و امن مدیریت شود و نباید مقادیر کنترلنشده کاربر مستقیماً در Header قرار بگیرند.
# CSRF در فرم آپلود
فرم آپلود نیز مانند سایر فرمهای تغییر وضعیت میتواند نیازمند محافظت در برابر CSRF باشد.
برای مثال:
Upload Form
↓
CSRF Token
↓
Authentication
↓
Authorization
↓
File Validation
↓
Storage
بنابراین اگر سیستم شما Session-Based Authentication دارد، محافظت CSRF را نیز در نظر بگیرید.
برای مطالعه بیشتر درباره این موضوع میتوانید مقاله «CSRF در PHP» را در کلاستر امنیت PHP مطالعه کنید.
# احراز هویت و مجوز دسترسی
فرض کنید فقط کاربران واردشده میتوانند فایل آپلود کنند.
قبل از آپلود باید بررسی شود:
if (!isset($_SESSION['user_id'])) {
exit('ابتدا وارد حساب کاربری شوید.');
}
اما احراز هویت به تنهایی کافی نیست.
باید Authorization نیز بررسی شود.
مثلاً:
User A
↓
Can upload?
↓
Can access this upload directory?
↓
Can replace this file?
↓
Can delete this file?
ممکن است کاربر اجازه آپلود داشته باشد، اما اجازه حذف یا مشاهده فایلهای کاربران دیگر را نداشته باشد.
# تفاوت Authentication و Authorization در آپلود فایل
Authentication یعنی:
> کاربر چه کسی است؟
مثلاً:
User ID = 25
Authorization یعنی:
> این کاربر اجازه انجام چه کاری را دارد؟
مثلاً:
User 25:
Upload → Yes
View own files → Yes
View user 50 files → No
Delete admin files → No
در سیستمهای واقعی هر دو مورد باید بررسی شوند.
# خطاهای رایج در پیادهسازی Upload
1. اعتماد به پسوند فایل
if ($extension === 'jpg') {
// safe
}
پسوند به تنهایی کافی نیست.
2. اعتماد به MIME ارسالشده توسط کاربر
$_FILES['file']['type']
نباید تنها معیار تشخیص نوع فایل باشد.
3. استفاده از نام اصلی فایل
$_FILES['file']['name']
نباید مستقیماً به عنوان نام فایل نهایی استفاده شود.
4. ذخیره فایل در Web Root بدون کنترل
این کار مخصوصاً برای فایلهای حساس میتواند مشکلساز باشد.
5. اجازه آپلود PHP
اگر سیستم فقط تصویر نیاز دارد، نیازی به فایل PHP ندارد.
6. استفاده از 777
Permission:
777
راهکار مناسبی برای Production نیست.
7. محدود نکردن حجم فایل
یک فایل بسیار بزرگ میتواند باعث مصرف منابع سرور شود.
8. محدود نکردن تعداد Upload
آپلود فایلهای کوچک متعدد نیز میتواند فضای دیسک را پر کند.
9. اعتماد به accept در HTML
این:
accept="image/*"
فقط یک کمک سمت مرورگر است و مکانیزم امنیتی نیست.
10. Extract کردن ZIP بدون بررسی مسیرها
این کار میتواند خطر Path Traversal ایجاد کند.
# چکلیست امنیت آپلود فایل در PHP
قبل از انتشار سیستم آپلود فایل، این موارد را بررسی کنید:
- [ ] فرم از
multipart/form-dataاستفاده میکند. - [ ] خطای
$_FILESبررسی میشود. - [ ] اندازه فایل محدود شده است.
- [ ] MIME Type روی سرور بررسی میشود.
- [ ] پسوند فایل Whitelist شده است.
- [ ] محتوای فایل در صورت نیاز بررسی میشود.
- [ ] برای تصاویر از بررسی تصویر استفاده شده است.
- [ ] نام فایل توسط سرور تولید میشود.
- [ ] از نام اصلی فایل برای مسیر ذخیرهسازی استفاده نمیشود.
- [ ] فایلهای خصوصی خارج از Web Root ذخیره میشوند.
- [ ] فایلهای آپلودشده قابلیت اجرای کد ندارند.
- [ ] Permissionهای مناسب برای پوشهها تنظیم شده است.
- [ ] از
777استفاده نشده است. - [ ] تعداد Uploadها محدود شده است.
- [ ] Rate Limiting در سیستمهای عمومی در نظر گرفته شده است.
- [ ] CSRF برای فرمهای مناسب فعال است.
- [ ] Authentication و Authorization بررسی میشوند.
- [ ] فایلهای ZIP با دقت Extract میشوند.
- [ ] SVG با احتیاط مدیریت میشود.
- [ ] در سیستمهای حساس اسکن فایل در نظر گرفته شده است.
- [ ] دانلود فایلها نیز کنترل دسترسی دارد.
- [ ] مسیر فایل از ورودی مستقیم کاربر دریافت نمیشود.
- [ ] فایلهای آپلودشده هرگز نباید به عنوان کد اجرا شوند.
# بهترین معماری برای سیستم آپلود فایل PHP
اگر بخواهیم تمام موارد بالا را در یک معماری ساده خلاصه کنیم:
User
│
▼
Authentication
│
▼
CSRF Check
│
▼
Receive Upload
│
▼
Upload Error
│
▼
Size Check
│
▼
Detect Real MIME
│
▼
Extension Whitelist
│
▼
Content Validation
│
▼
Generate Random Name
│
▼
Store Outside Web Root
│
▼
Database Record
│
▼
Controlled File Download
این معماری باعث میشود امنیت آپلود فقط به یک شرط ساده مانند بررسی پسوند وابسته نباشد.
# جمعبندی
امنیت آپلود فایل در PHP موضوعی بسیار مهم است، زیرا فایل آپلودشده مستقیماً وارد زیرساخت برنامه میشود.
اشتباهاتی مانند:
$_FILES['file']['name']
یا:
$_FILES['file']['type']
به تنهایی نمیتوانند امنیت سیستم آپلود را تضمین کنند.
یک سیستم مناسب باید چند لایه دفاعی داشته باشد:
File Size
+
MIME Type
+
Extension
+
Content Validation
+
Random Filename
+
Safe Storage
+
No Execution
+
Authentication
+
Authorization
+
CSRF
+
Rate Limiting
همچنین باید به خاطر داشته باشیم که هیچکدام از این کنترلها به تنهایی تضمینکننده امنیت کامل نیستند. امنیت واقعی حاصل ترکیب چند لایه دفاعی و طراحی صحیح معماری برنامه است.
اگر در حال توسعه یک پروژه PHP هستید، بهتر است قابلیت آپلود فایل را از ابتدا به شکل امن طراحی کنید، نه اینکه بعداً پس از انتشار برنامه تلاش کنید مشکلات امنیتی آن را برطرف کنید.
در ادامه کلاستر امنیت PHP، موضوعاتی مانند Session Fixation، Session Hijacking و امنیت Session در PHP میتوانند لایه بعدی امنیت احراز هویت کاربران را پوشش دهند.





