💡 مثال کامل قابل اجرا در GitHub موجود است:
load-untrusted-documents-safely-python

روش قدیمی دردناک بود

شما سه خط کد نوشتید تا تصویر بندانگشتی یک سند بارگذاری‌شده را رندر کنید. این خطوط به این شکل بودند و به‌نظر درست می‌آمدند:

with signature.Signature(upload_path) as sign:
    save_page_preview(sign, thumbnail_path)

آنچه این خطوط قبل از GroupDocs.Signature 26.9 انجام می‌داد، دریافت هر آدرسی بود که سند به آن اشاره می‌کرد. یک فایل Word می‌تواند تصویری را که در خود ندارد نگه دارد – فایل فقط یک URL ذخیره می‌کند و هر برنامه‌ای که آن را باز می‌کند، آن URL را دانلود می‌کند. در یک دسکتاپ این یک ویژگی است. اما در سروری که بارگذاری‌ها را می‌پذیرد، به این معنی است که شخصی که فایل را برای شما می‌فرستد، تصمیم می‌گیرد سرور شما به چه آدرسی درخواست بفرستد.

این حمله نام دارد Server‑Side Request Forgery (SSRF) و سه شکل دارد که باید نام‌گذاری شوند. یک آدرس داخلی که از اینترنت قابل دسترسی نیست، از سرور شما قابل دسترسی است، بنابراین یک سند دست‌ساز می‌تواند سرویس شما را به دریافت http://169.254.169.254/ یا یک نقطهٔ انتهایی مدیریتی روی localhost وادار کند. یک مسیر UNC می‌تواند یک میزبان ویندوزی را به احراز هویت خروجی وادار کند و اعتبارها را به سرور کنترل‌شده توسط مهاجم بدهد. و یک لینک به میزبان‌ایی که هرگز پاسخ نمی‌دهد، نخ بارگذاری را تا زمان timeout نگه می‌دارد؛ این یک روش ارزان برای خسته کردن استخر کارگرها با اسنادی است که به‌نظر بی‌خطر می‌آیند.

هیچ‌یک از این موارد باگ در کتابخانهٔ سند نیست. دنبال کردن لینک همان کاری است که فرمت درخواست می‌کند. بخش ناخوشایند این بود که این رفتار به‌صورت پیش‌فرض فعال بود و در کد هیچ‌کس آن را در مرور کدها پرچم نمی‌زد.

راه بهتر وجود دارد

بارگذاری امن سند، رفتار پیش‌فرض GroupDocs.Signature برای Python است که از انجام این درخواست‌ها خودداری می‌کند. از نسخه 26.9، مقدار پیش‌فرض LoadOptions.skip_external_resources برابر True است، بنابراین همان سه خط اکنون هیچ‌چیزی دریافت نمی‌کنند و به‌جای تصویر لینک‌شده، یک جای‌دار (placeholder) نمایش می‌دهد.

این تغییر یک مقدار پیش‌فرض است نه یک ویژگی جدید – این خصوصیت قبلاً وجود داشت. چیزی که در 26.9 تغییر کرد این است که وقتی کد شما چیزی تنظیم نمی‌کند، به کدام حالت اشاره می‌کند؛ این همان تنظیمی است که اکثر سرویس‌ها به‌طور پیش‌فرض استفاده می‌کنند.

روش جدید: سه حالت بارگذاری

گام 1 – برای هر چیزی که غیرقابل اعتماد است، پیش‌فرض را حفظ کنید

هیچ LoadOptions ای استفاده نکنید:

with signature.Signature(source_path) as sign:
    return save_page_preview(sign, preview_path)

هیچ درخواستی ارسال نمی‌شود. پیش‌نمایش کوچکتر از حالت عادی است و این تفاوت اندازه، واضح‌ترین مدرکی است که نشان می‌دهد هیچ درخواست‌ایی از ماشین خارج نشده است.

گام 2 – یک میزبان که واقعاً متعلق به شماست را در فهرست سفید بگذارید

اسناد زیادی به مکان‌های معتبر لینک می‌شوند: CDN شرکت، سرور تصویر داخلی، فروشگاه قالب‌ها. فقط آن را اجازه دهید و هیچ چیز دیگری را:

load_options = LoadOptions()
load_options.whitelisted_resources = [trusted_address]

with signature.Signature(source_path, load_options) as sign:
    return save_page_preview(sign, preview_path)

قانون تطبیق نیاز به توجه دارد. این یک تست زیررشته‌ای بدون حساسیت به حروف بزرگ/کوچک بر روی آدرس منبع است، که باعث می‌شود یک بخش کوتاه خطرناک باشد: github هم‌چنان github.attacker.example/payload.png را همانند میزبان موردنظر شما می‌پذیرد. از یک طرح (scheme)، یک میزبان و یک مسیر استفاده کنید – این نمونه raw.githubusercontent.com/groupdocs-signature/ را در فهرست سفید می‌گذارد.

گام 3 – همه چیز را به‌صورت عمدی اجازه دهید

رفتار پیش از نسخه 26.9 که هنوز در دسترس است:

load_options = LoadOptions()
load_options.skip_external_resources = False

برای اسنادی که برنامهٔ خودتان تولید کرده مناسب است. یک تله: خصوصیت منسوخ‌شده load_external_resources قطبیت مخالف دارد، بنابراین skip_external_resources = False معادل load_external_resources = True است. اگر مقدار را از خصوصیت قدیمی کپی کنید، بدون هیچ خطایی وضعیت امنیتی خود را معکوس می‌کنید.

مقایسهٔ کنار هم: قبل vs. بعد

همان سند، همان مسیر کد، سه سیاست بارگذاری. این‌ها اندازهٔ فایل‌های موجود در پوشهٔ Result/ نمونه هستند، بنابراین می‌توانید به‌جای اعتماد، آن‌ها را بررسی کنید:

حالت بارگذاری اندازهٔ پیش‌نمایش درخواست‌های خروجی
پیش‌فرض (26.9 و بعد) 16,435 بایت هیچ‌کدام
میزبان فهرست‌سفید 51,738 بایت یک درخواست به آدرس مجاز
همهٔ منابع (پیش‌فرض قبل از 26.9) 51,738 بایت یک درخواست برای هر منبع لینک‌شده

تصویر لینک‌شده 35,303 بایت از این تفاوت را تشکیل می‌دهد. تا زمانی که این دو عدد را کنار هم نداشتم، به تنظیمات اعتماد نداشتم و همین‌طور پیشنهاد می‌کنم: خواندن مقدار خصوصیت به شما می‌گوید چه چیزی را پیکربندی کرده‌اید، نه چه کاری انجام شده است.

چه چیزی به‌عنوان منبع خارجی محسوب می‌شود؟

ناراحت‌کننده‌تر از آنچه مردم انتظار دارند، به همین دلیل ارتقا معمولاً بدون مشکل است. تصاویر لینک‌شده به‌جای تعبیه‌شده، فیلدهای INCLUDEPICTURE، تصاویر لینک‌شده در ارائه‌ها و صفحات گسترده، و تصاویر و سبک‌برگ‌هایی که یک SVG به آن‌ها ارجاع می‌دهد. محتویات تعبیه‌شده دست‌نخورده می‌مانند، چون قبلاً داخل فایل هستند و برای رندر کردن نیازی به درخواست ندارند.

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

مثال واقعی: بارگذاری‌ای که امضا می‌شود

موردی که تغییر پیش‌فرض برای آن وجود دارد. یک سند از بیرون می‌آید و شما باید امضایی روی آن بگذارید:

with signature.Signature(source_path) as sign:
    options = QrCodeSignOptions("Approved by GroupDocs.Signature")
    options.encode_type = QrCodeTypes.QR
    options.left = 400
    options.top = 50
    options.width = 120
    options.height = 120

    result = sign.sign(output_path, options)

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

چه چیزهای دیگری هنگام ارتقا تغییر می‌کند؟

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

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

یک جزئیات پایتون: نحوهٔ نوشتن پیش‌نمایش

PreviewOptions به‌جای مسیر، دو کارخانهٔ جریان (stream factory) می‌گیرد و فقط Callableهای سادهٔ پایتون کافی هستند:

def create_page_stream(page_data):
    return open(preview_path, "wb")

def release_page_stream(page_data, page_stream):
    page_stream.close()

preview_options = PreviewOptions(create_page_stream, release_page_stream)
preview_options.preview_format = PreviewOptions.PreviewFormats.PNG
sign.generate_preview(preview_options)

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

نتیجه‌گیری

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

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

اجرای نمونه بر روی یکی از اسناد خودتان یک دقیقه طول می‌کشد و در سه اندازهٔ فایل دقیقاً به شما می‌گوید سرویس شما چه چیزی را به‌نام فرستندهٔ فایل دریافت کرده است.

منابع تکمیلی