سی ام اس نویسسی ام اس نویسیاد بگیر، سیستم خودتو بنویس
مجلهتماس با ما
سی ام اس نویسسی ام اس نویس

CMS نویس مرجعی برای آموزش برنامه نویسی وب و PHP، توسعه افزونه‌های اختصاصی وردپرس، طراحی ورک‌فلو و اتوماسیون با n8n و توسعه CMS های اختصاصی است. هدف ما ارائه آموزش‌های کاربردی و راهکارهای حرفه‌ای برای توسعه‌دهندگان، کسب‌وکارها و علاقه‌مندان به دنیای برنامه‌نویسی است.

پیوندهای سریع
محصولات فیزیکیمحصولات دیجیتالدوره‌های آموزشی
سی ام اس نویس
مجله سی ام اس نویستماس با ماپنل کاربری
سی ام اس نویس — تمامی حقوق برای سی ام اس نویس محفوظ است. © 1406 تماس با ما09307711941
امنیت آپلود فایل در PHP چیست؟ آموزش جلوگیری از File Upload Vulnerability

امنیت آپلود فایل در PHP چیست؟ آموزش جلوگیری از File Upload Vulnerability

ممدیر کل سیستم ۲۶ شهریور ۱۴۰۵ ۲۳ دقیقه مطالعه ۱ بازدید
امنیت آپلود فایل در PHP چیست؟ آموزش جلوگیری از File Upload Vulnerability

آپلود فایل یکی از قابلیت‌های رایج در بسیاری از وب‌سایت‌ها و برنامه‌های 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:

  1. کاربر را احراز هویت می‌کند.
  2. بررسی می‌کند اجازه دسترسی به فایل را دارد یا خیر.
  3. فایل مورد نظر را پیدا می‌کند.
  4. آن را به کاربر ارسال می‌کند.

این روش برای فایل‌های خصوصی مثل:

  • مدارک
  • فاکتورها
  • فایل‌های خریداری‌شده
  • فایل‌های کاربران
  • فایل‌های آموزشی خصوصی

مناسب‌تر است.


# استفاده از 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

این روش مخصوصاً برای سیستم‌هایی که کاربران می‌توانند فایل‌های مختلفی مانند:

  • PDF
  • 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 می‌توانند لایه بعدی امنیت احراز هویت کاربران را پوشش دهند.

نظرات (0)

نظرات پس از تأیید مدیر نمایش داده می‌شوند

هنوز نظری ثبت نشده است؛ اولین نفر باشید!

مقالات مرتبط

ساخت CMS با Laravel 12؛ آموزش ساخت سیستم مدیریت محتوا از صفر

ساخت CMS با Laravel 12؛ آموزش ساخت سیستم مدیریت محتوا از صفر

۲۴ دقیقه
ساخت فروشگاه اینترنتی با Laravel 12؛ آموزش پروژه‌محور از صفر تا طراحی هسته فروشگاه

ساخت فروشگاه اینترنتی با Laravel 12؛ آموزش پروژه‌محور از صفر تا طراحی هسته فروشگاه

۲۲ دقیقه
ساخت Marketplace با Laravel؛ آموزش طراحی فروشگاه چندفروشندگی

ساخت Marketplace با Laravel؛ آموزش طراحی فروشگاه چندفروشندگی

۲۲ دقیقه
مدیریت Queueهای سنگین در Laravel؛ آموزش پردازش کارهای طولانی

مدیریت Queueهای سنگین در Laravel؛ آموزش پردازش کارهای طولانی

۲۲ دقیقه