💡 مثال كامل يعمل متاح على GitHub:
load-untrusted-documents-safely-python
الطريقة القديمة كانت مؤلمة
كتبت ثلاث أسطر لتوليد صورة مصغرة لمستند تم تحميله. كانت تبدو هكذا، وكانت تبدو صحيحة:
with signature.Signature(upload_path) as sign:
save_page_preview(sign, thumbnail_path)
ما كانت تفعله هذه الأسطر، قبل الإصدار 26.9 من GroupDocs.Signature، هو جلب كل عنوان يشير إليه المستند. يمكن لملف Word أن يحتوي على صورة لا يحملها فعليًا – فالملف يخزن عنوان URL، وأي برنامج يفتحه يقوم بتنزيل ذلك العنوان. على سطح المكتب هذا يُعد ميزة. على خادم يقبل التحميلات، يعني ذلك أن الشخص الذي أرسل لك الملف يقرر أي عناوين تطلبها بنيتك التحتية.
الهجوم له اسم، server‑side request forgery، وله ثلاث أشكال تستحق الذكر. عنوان داخلي غير قابل للوصول من الإنترنت يمكن الوصول إليه من خادمك، لذا يمكن لمستند مُصمم أن يجعل خدمتك تجلب http://169.254.169.254/ أو نقطة نهاية إدارية على localhost. مسار UNC يمكن أن يدفع مضيف Windows إلى المصادقة الصادرة، مما يسلم بيانات الاعتماد إلى خادم يتحكم فيه المهاجم. ورابط إلى مضيف لا يرد أبداً يبقي خيط التحميل معلقًا حتى ينتهي مهلة الانتظار، وهو طريقة رخيصة لاستنزاف مجموعة العمال بوثائق تبدو غير ضارة.
لا شيء من ذلك يُعد خطأً في مكتبة المستندات. اتباع الرابط هو ما يطلبه التنسيق. الجزء غير المريح هو أن الإلتزام بذلك كان الإعداد الافتراضي، وفي الشيفرة لا أحد يعلّمه في المراجعة.
هناك طريقة أفضل
التحميل الآمن للمستند هو سلوك GroupDocs.Signature للبايثون الذي يرفض إجراء تلك الطلبات. بدءًا من الإصدار 26.9، القيمة الافتراضية للخاصية LoadOptions.skip_external_resources هي True، لذا الآن الثلاث أسطر نفسها لا تجلب شيئًا وتُظهر عنصرًا نائبًا حيث كانت ستظهر الصورة المرتبطة.
التغيير هو إعداد افتراضي وليس ميزة جديدة – الخاصية كانت موجودة بالفعل. ما غيره الإصدار 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. إذا نسخت قيمة من الخاصية القديمة ستقلب وضع أمانك دون أي خطأ يُخبرك بذلك.
جنبًا إلى جنب: قبل مقابل بعد
نفس المستند، نفس مسار الشيفرة، ثلاث سياسات للتحميل. هذه هي أحجام الملفات الموجودة في مجلد 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 يأخذ مصنعين للتيارات بدلاً من مسار، والدالات القابلة للاستدعاء في بايثون هي كل ما يحتاجه:
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)
واحد يُنشئ تدفقًا لكل صفحة، والآخر يُغلقه. المستند العيني يحتوي صفحة واحدة، لذا يُكتب ملف واحد؛ بالنسبة للمدخلات متعددة الصفحات، أدرج رقم الصفحة في الاسم أو سيكتب كل صفحة فوق السابقة.
الخلاصة
تم عكس الإعداد الافتراضي بحيث يصبح السلوك المخاطر يتطلب قرارًا صريحًا، والسلوك الآمن لا يحتاج إلى شيء. احتفظ بالإعداد الافتراضي للمدخلات غير الموثوقة، وضع في القائمة البيضاء بشكل ضيق عندما تكون المضيفات الخاصة بك متورطة، وتذكر أن التوقيع لم يحتاج أبدًا إلى الشبكة.
إذا أردت فحصًا أقوى من حجم الملف، وجه مستند اختبار إلى مضيف تملكه وراقب سجل الوصول أثناء تشغيل المعاينة. الحجم يخبرك ما إذا وصلت البايتات؛ سجل الوصول يخبرك ما إذا تم إجراء طلب أصلاً، وهما يختلفان بالضبط في الحالة التي تهم – المضيف في القائمة البيضاء لكنه غير قابل للوصول يبدو مماثلًا للمضيف المحظور من ناتج المعاينة وحده.
تشغيل العينة على أحد مستنداتك الخاصة يستغرق دقيقة واحدة ويخبرك، بثلاث أحجام ملفات، بالضبط ما الذي كان خدمتك تجلبه نيابةً عن من أرسل لك الملف.