چرا پسوند فایل برای اعتماد کافی نیست؟

وقتی کاربر عکس بارگذاری می‌کند، نام فایل و نوع اعلام‌شدهٔ مرورگر به‌تنهایی مبنای اعتماد نیستند. یک فایل کوچک هم ممکن است پس از بازشدن، تعداد زیادی پیکسل داشته باشد و منابع را مصرف کند. ابتدا مشخص کنید محصول واقعاً چه قالب‌ها و چه اندازه‌ای لازم دارد. در این مقاله، دریافت آواتار را به تصویر ثابت محدود می‌کنیم و هستهٔ اعتبارسنجی را جدا از چارچوب وب آزمایش می‌کنیم.

نمونه پیش از بازکردن تصویر، تعداد بایت خوانده‌شده را محدود می‌کند. پس از شناسایی قالب واقعی، تعداد پیکسل و متحرک‌نبودن بررسی می‌شود و سپس تصویر بازخوانی و به خروجی کنترل‌شده تبدیل می‌شود. فایل اس‌وی‌جی یا قالب‌های دیگر در این قرارداد پذیرفته نیستند. این محدودیت‌ها همهٔ خطرهای پردازش تصویر را حذف نمی‌کنند؛ کتابخانه و محیط اجرا باید به‌روز و دارای بودجهٔ مناسب حافظه و زمان باشند.

بودجهٔ دریافت و تبدیل کنترل‌شدهٔ تصویر

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

چهار فایل را کنار هم ذخیره و Pillow را در محیط مجازی نصب کنید. با python -m unittest test_avatar_store.py، ورودی واقعی تصویر، نام تولیدشده، تغییر اندازه، فایل نامعتبر و عبور از سقف حجم بررسی می‌شود. سپس python demo_upload.py را اجرا کنید؛ نمونه یک تصویر آزمایشی در حافظه می‌سازد و نام خروجی وب‌پی را چاپ می‌کند. هیچ فایل واقعی کاربر یا نشانی اینترنتی برای این آزمایش دریافت نمی‌شود.

این تابع یک نقطهٔ پایانی کامل وب نیست. احراز هویت، مجوز کاربر، محافظت درخواست، محدودیت تعداد بارگذاری، فضای ذخیره‌سازی و ثبت رویداد باید در لایهٔ برنامه اجرا شوند. برای نمایش فایل، نوع محتوای درست و جلوگیری از حدس‌زدن نوع را تنظیم کنید و در صورت نیاز دامنهٔ جدا برای محتوای کاربران داشته باشید. آزمون موفق تبدیل تصویر، مجوز حذف این کنترل‌ها یا تضمین نبود همهٔ محتوای مخرب نیست.

فایل‌های کامل و قابل اجرا

چهار فایل زیر کامل‌اند؛ پیش‌نیاز، ترتیب اجرا و روش بررسی در متن آمده است. کدها خلاصه یا پنهان نشده‌اند.

1. فایل کامل policy.json

این فایل را با همین نام و بدون حذف سطرها کنار بقیهٔ فایل‌ها ذخیره کنید.

{"maxBytes": 1000000, "maxPixels": 4000000, "maxWidth": 1000}

دستور اجرا، نتیجهٔ مورد انتظار و محدودیت این فایل در بخش اجرای مقاله توضیح داده شده است.

2. فایل کامل avatar_store.py

این فایل را با همین نام و بدون حذف سطرها کنار بقیهٔ فایل‌ها ذخیره کنید.

from io import BytesIO
from pathlib import Path
import uuid
from PIL import Image, ImageOps


def store_avatar(stream, directory, policy):
    data = stream.read(policy['maxBytes'] + 1)
    if len(data) > policy['maxBytes']:
        raise ValueError('Upload exceeds byte limit')
    with Image.open(BytesIO(data)) as original:
        if original.format not in {'JPEG', 'PNG'} or getattr(original, 'is_animated', False):
            raise ValueError('Only single JPEG or PNG images are accepted')
        if original.width * original.height > policy['maxPixels']:
            raise ValueError('Image exceeds pixel limit')
        original.load()
        image = ImageOps.exif_transpose(original).convert('RGB')
        image.thumbnail((policy['maxWidth'], policy['maxWidth']))
        root = Path(directory); root.mkdir(mode=0o700, parents=True, exist_ok=True)
        target = root / (uuid.uuid4().hex + '.webp')
        with target.open('xb') as output:
            image.save(output, format='WEBP', quality=85)
    return target.name

دستور اجرا، نتیجهٔ مورد انتظار و محدودیت این فایل در بخش اجرای مقاله توضیح داده شده است.

3. فایل کامل demo_upload.py

این فایل را با همین نام و بدون حذف سطرها کنار بقیهٔ فایل‌ها ذخیره کنید.

from io import BytesIO
import json
from pathlib import Path
from PIL import Image
from avatar_store import store_avatar

if __name__ == '__main__':
    policy = json.loads(Path(__file__).with_name('policy.json').read_text())
    incoming = BytesIO(); Image.new('RGB', (600, 300), 'green').save(incoming, format='PNG'); incoming.seek(0)
    print(store_avatar(incoming, 'private-avatars', policy))

دستور اجرا، نتیجهٔ مورد انتظار و محدودیت این فایل در بخش اجرای مقاله توضیح داده شده است.

4. فایل کامل test_avatar_store.py

این فایل را با همین نام و بدون حذف سطرها کنار بقیهٔ فایل‌ها ذخیره کنید.

from io import BytesIO
from pathlib import Path
import tempfile
import unittest
from PIL import Image, UnidentifiedImageError
from avatar_store import store_avatar


class UploadTests(unittest.TestCase):
    def test_reencode_and_generated_name(self):
        with tempfile.TemporaryDirectory() as directory:
            incoming = BytesIO(); Image.new('RGB', (60, 30)).save(incoming, format='PNG'); incoming.seek(0)
            name = store_avatar(incoming, directory, {'maxBytes': 10000, 'maxPixels': 10000, 'maxWidth': 40})
            self.assertRegex(name, r'^[a-f0-9]{32}\.webp$')
            with Image.open(Path(directory) / name) as image: self.assertEqual(image.size, (40, 20))

    def test_invalid_bytes_and_size(self):
        with tempfile.TemporaryDirectory() as directory:
            policy = {'maxBytes': 10, 'maxPixels': 10000, 'maxWidth': 40}
            with self.assertRaises(ValueError): store_avatar(BytesIO(b'x' * 11), directory, policy)
            with self.assertRaises(UnidentifiedImageError): store_avatar(BytesIO(b'fake.jpg'), directory, policy)


if __name__ == '__main__': unittest.main()

دستور اجرا، نتیجهٔ مورد انتظار و محدودیت این فایل در بخش اجرای مقاله توضیح داده شده است.

اجرای نمونه و مرز مسئولیت سرویس

در محصول واقعی، نمونه‌های بزرگ، فایل بریده‌شده، تصویر متحرک، خطای نوشتن دیسک و درخواست‌های هم‌زمان را هم بررسی کنید. خطای اعتبارسنجی باید پیام روشن داشته باشد و دادهٔ خصوصی یا مسیر داخلی سرور را افشا نکند. محدودیت‌ها را با نیاز آواتار تنظیم کنید؛ دریافت سند یا ویدیو قرارداد دیگری می‌خواهد. هدف، یک مسیر کوچک و قابل توضیح از ورودی محدود تا خروجی معتبر است که در کنار کنترل‌های سرویس کار کند.

چک‌لیست اجرای عملی

  • حجم بایت و تعداد پیکسل هر دو محدود باشند.
  • نام و مسیر خروجی از کاربر گرفته نشوند.
  • نوع واقعی و متحرک‌نبودن بررسی شوند.
  • احراز هویت و کنترل درخواست در سرویس حفظ شوند.

منابع

تدوین و راهنمای عملی: دانشنامه لیان.