مقال تقني

معاينة آمنة لملفات PDF في تطبيقات Delphi باستخدام مكون PDFium

إن معاينة ملف PDF غير موثوق به داخل تطبيقك الخاص هو قرار تنفيذي، والجزء الذي يهم ليس واجهة العارض بل ما يرفض الجزء القيام به بمفرده. لا تكتب الملف على القرص. لا تدع روابطه تستدعي أوامر النظام. لا تمنح مرفقاته مسارًا. معظم الأضرار الناجمة عن مستند معادٍ لا تأتي من استغلال المحرك بل من عارض يقوم بأشياء عادية تمامًا باستخدام مدخلات يقدمها المهاجم: فتح رابط file:// إلى مشاركة UNC يسرب بيانات اعتماد NTLM، أو ترك نسخة مؤقتة في دليل الملفات المؤقتة، أو نسخ الحمولات المضمنة إلى أي مكان يحدده اسم الملف. مكون PDFium هو عارض PDF برمجي المصدر لـ Delphi و C++Builder و Lazarus، وهو يضع المفاتيح ذات الصلة حيث يمكنك الوصول إليها: علامة وقت التحميل التي توقف البرمجة النصية، وأحداث النقر على الروابط التي يمكنك رفضها، والوصول إلى المرفقات الذي يعمل من خلال الكود الخاص بك، وبتات الأذونات التي يمكنك قراءتها. يتتبع الترتيب أدناه المستند منذ لحظة وصوله إلى لحظة نقر المستخدم على شيء ما فيه

نموذج التهديد لجزء المعاينة

كن صادقًا بشأن ما تقدمه لك "المعاينة الآمنة". يحلل المصير بايتات غير موثوقة بغض النظر عما تفعله، وتحصين المحرك نفسه هو الأساس الذي تقف عليه. كل شيء فوق ذلك الأساس هو سياسة التطبيق: ما إذا كانت البرامج النصية تبدأ، وما يفعله النقر على الرابط، وما إذا كانت الملفات المضمنة يمكنها الوصول إلى القرص، وما إذا كانت الحافظة والطابعة أبوابًا أم جدرانًا. هناك شيء واحد يجب استبعاده مبكرًا وهو مفتاح FPDF_SetSandBoxPolicy الخاص بالمحرك. معظم قيود المحرك مدمجة فيه، والمفتاح يغير القليل في الممارسة العملية، وتخصيص أي جزء من قصة العزل الخاصة بك له ينتج فقط إحساسًا زائفًا بإنجاز شيء ما. عندما يكون الإدخال معاديًا بشكل حقيقي، على سبيل المثال بوابة تحميل عامة، فإن العزل الحقيقي الوحيد هو التجسيد في عملية منفصلة منخفضة الامتيازات وإرسال الصور النقطية إلى واجهة المستخدم. العلامات داخل العملية هي سياسة. إنها ليست احتواءً

هناك سطحان يسهل نسيانهما تحديدًا لأنه لا توجد نقرة تلمسهما أبدًا. الأول هو الملفات المؤقتة. إذا كان مسار العمل الخاص بك يخزن المستندات الواردة على القرص قبل معاينتها، فإن تلك النسخ المخزنة تعيش أطول من الجلسة ما لم يقم شيء بحذفها بشكل يمكن التحقق منه، والملف "القابل للاسترداد من الدليل المؤقت" هزم بهدوء كل تحكم يفرضه الجزء نفسه. قم بالتحميل من الذاكرة عبر TPdfStreamAdapter بدلاً من ذلك، بحيث لا تحصل البايتات المعادية أبدًا على مسار خاص بها. الثاني هو الحافظة. المعاينة التي تسمح بالتحديد والنسخ قد قامت بالفعل بتصدير المستند، شاشة واحدة في كل مرة، ولن يلتقط أي اعتراض للروابط ذلك

إيقاف JavaScript في وقت التحميل، وليس في واجهة المستخدم

يتم تهيئة JavaScript الخاص بالمستند في مكون PDFium فقط مع بيئة تعبئة النموذج. وبالتالي، فإن التحميل باستخدام FormFill := False يعطل البرمجة النصية من جذورها بدلاً من قمع أعراضها:

procedure TPreviewPane.LoadUntrusted(const FilePath: string);
begin
  Pdf.FileName := FilePath;
  Pdf.FormFill := False;     // no form environment, hence no JavaScript engine
  Pdf.Active := True;

  FPermissions := Pdf.Permissions;   // raw flag word; all bits set = unrestricted
end;

المقايضة حقيقية وتنتمي إلى مواصفاتك. مع تعطيل تعبئة النماذج، تختفي أيضًا برامج التفاعل والتحقق النصية المشروعة الخاصة بـ AcroForm؛ يتم عرض الحقول بمظهرها الأخير المحفوظ ولكن لا يمكن تعديلها. بالنسبة لجزء المعاينة، يكون هذا عادةً القرار الصحيح، لأن المعاينة تعني النظر، وليس التعبئة. ولكن إذا كانت نفس النافذة تعمل أيضًا كسطح لتعبئة النماذج للمستندات الداخلية الموثوقة، فإن الحل هو مساران للتحميل مع قرار ثقة صريح بينهما، وليس مسارًا واحدًا بإعداد توفيقي فضفاض للغاية بالنسبة للحالة المعادية وصارم للغاية بالنسبة للحالة الموثوقة. الجانب الخاص بتعبئة النماذج من هذا التقسيم له فخاخه الخاصة، والتي يتم تناولها في التنقل في حقل النموذج وتجديد المظهر

الروابط: المعالج الافتراضي يستدعي النظام

إذا تُركت بدون تدخل، تذهب نقرات الروابط مباشرة إلى نظام التشغيل. تتضمن LinkOptions الافتراضية للعارض loAutoOpenURI، وهو تسرب file:// إلى مشاركة UNC ينتظر الحدوث. يشكل حدثان نقطة الاختناق: OnWebLinkClick لعناوين URL المكتشفة في نص الصفحة، و OnAnnotationLinkClick للتعليقات التوضيحية للروابط التي تحمل URI أو إجراءات الإطلاق. قم بتعيين Handled := True في كليهما، دون قيد أو شرط، قبل اتخاذ أي قرار، ثم أعد السماح فقط بما تسمح به السياسة. كطبقة ثانية، أسقط loAutoOpenURI من LinkOptions للإدخال المعادي وتأكد من أن loAutoLaunch، المتوقف افتراضيًا، لا يتسلل أبدًا مرة أخرى من خلال تكوين منسوخ:

procedure TPreviewPane.PdfViewWebLinkClick(Sender: TObject;
  const Url: WString; var Handled: Boolean);
begin
  Handled := True;   // never fall through to the default shell behavior

  if (AnsiStartsText('https://', Url) or AnsiStartsText('http://', Url))
    and HostIsAllowed(Url) then
    OpenInBrowser(Url)
  else
    FAudit.LogBlockedLink(FDocumentId, Url);
end;

هناك تفصيلان يقرران ما إذا كان هذا يصمد حقًا. أولاً، يجب أن يكون فحص المخطط فحصًا للبادئة على السلسلة الأولية قبل أي تحليل، لأن file:// ومسارات UNC والمخططات الغريبة هي بالضبط القيم التي تعطل محلل URL الساذج أو تفلت من محلل يقوم بالتسوية بشكل مفرط. ثانيًا، سجل كل حظر مع إرفاق هوية المستند. القليل من روابط file:// المحظورة هي ضوضاء في الخلفية؛ انفجار منها عبر العديد من المستندات الواردة في نافذة قصيرة هو حادث يفضل فريق الأمان لديك أن يسمع عنه منك بدلاً من مكان آخر

المرفقات: سياسة الامتداد واسم الملف الذي لم تختره

ملف PDF هو حاوية، و AttachmentCount مع خاصية AttachmentName[] يخبرانك بما يحمله قبل أن يلمس أي شيء القرص. هناك عنصري تحكم منفصلين مهمين هنا، وواحد منهما فقط واضح. الواضح هو سياسة النوع: قائمة السماح للامتدادات التي يمكن تصديرها على الإطلاق. الخفي هو أن اسم المرفق هو بيانات يتحكم فيها المهاجم، نقطة. يحول اسم مضمن مثل ..\..\Startup\update.exe حفظًا غير مبالٍ إلى اجتياز مسار يسقط ملفًا تنفيذيًا في مجلد يشغله Windows عند تسجيل الدخول. يسلمك المكون الحمولة كبايتات من خلال Attachment[] ويترك التعليمات البرمجية الخاصة بك تختار المسار، لذا قم ببناء هذا المسار من اسم أساسي مطهر وأبدًا من السلسلة المضمنة الأولية:

procedure TPreviewPane.ExportAttachment(Index: Integer; const TargetDir: string);
var
  RawName, SafeName, Ext: string;
  Data: TBytes;
begin
  RawName := string(Pdf.AttachmentName[Index]);
  SafeName := ExtractFileName(RawName);    // strips any path components
  Ext := LowerCase(ExtractFileExt(SafeName));

  if not FAllowedExt.Contains(Ext) then    // allowlist, not blocklist
    raise EPreviewPolicy.CreateFmt('Attachment type %s blocked by policy', [Ext]);

  Data := Pdf.Attachment[Index];           // embedded payload as raw bytes
  TFile.WriteAllBytes(
    IncludeTrailingPathDelimiter(TargetDir) + SafeName, Data);
end;

فضل اتجاه قائمة السماح. القائمة السوداء للامتدادات "الخطيرة" هي سباق تخسره في اليوم الذي يقوم فيه شخص ما بتسليح امتداد لم تسمع به من قبل؛ قائمة السماح لـ .pdf و .png و .csv تفشل في حالة الإغلاق (الأمان الافتراضي)

ما تعد به أذونات التشفير فعليًا

يقوم معالج الأمان القياسي لـ ISO 32000-1 بتشفير علامات الأذونات للطباعة ونسخ المحتوى والتعديل، وتقوم الخصائص Permissions و UserPermissions بإظهارها كأقنعة بتات أولية بمجرد فتح المستند. يحدد جدول 22 من ISO 32000-1 البتات، ويقوم الملف غير المشفر بالإبلاغ عن تعيين كل بت. اقرأها واحترمها في طبقة الأوامر الخاصة بك، لكن كن واضحًا بشأن ما هي. بالنسبة لمستند مشفر بكلمة مرور مالك وكلمة مرور مستخدم فارغة، يتم فك تشفير المحتوى بالكامل عند الفتح، والعلامات هي طلب للعارضين المتوافقين، وليست آلية إنفاذ. لذلك نتيجتان، وهما تسحبان في اتجاهين متعاكسين. لا تقم أبدًا بتقديم علامات الأذونات للمستخدمين كخاصية أمنية للمستندات التي يتلقونها، لأنها ليست كذلك. في الوقت نفسه، احترم بت استخراج إمكانية الوصول (البت 10) حتى في حالة رفض النسخ العام (البت 5)؛ تم تخصيص وصول قارئ الشاشة بشكل منفصل في نموذج الأذونات عن قصد، وتجريده لأن "النسخ متوقف" يكسر التكنولوجيا المساعدة دون أي مكسب أمني

افرض الإجراءات المرفوضة على مستوى الأمر، وليس عن طريق إخفاء أزرار شريط الأدوات. Ctrl+C، وقوائم السياق، والتحديد بالسحب، كلها تتجاوز شريط الأدوات؛ فحص إذن واحد داخل أمر النسخ لا يتجاوز أي شيء

بالنسبة للمستندات التي تتطلب كلمة مرور مستخدم، قم بتعيين Password قبل Active := True وتعامل مع القيمة مثل السر الذي هي عليه: أحضرها من مخزن بيانات الاعتماد الخاص بك لكل جلسة، وأبعدها عن السجلات وتقارير الأعطال، ولا تحفظها أبدًا بجوار المستند. إن جزء المعاينة الذي يخزن كلمات المرور مؤقتًا "للراحة" أصبح بهدوء قاعدة بيانات كلمات مرور بدون أي من وسائل الحماية الخاصة بها

تستحق الطباعة قرارًا خاصًا بها بدلاً من وراثة أي شيء وصلت إليه قاعدة النسخ. إن المطبوعات المادية غير خاضعة للتدقيق بحكم تعريفها، ومع ذلك فإن حظر الطباعة تمامًا يميل إلى دفع المستخدمين نحو لقطات الشاشة، وهي أسوأ على كل المحاور. الحل الوسط الشائع هو السماح بالطباعة ولكن ختم كل صفحة بهوية المستخدم وطابع زمني، مفروضًا داخل أمر الطباعة. فقط احتفظ بالتوقع الصحيح لذلك: العلامة المائية هي ردع وإسناد. إنها ليست منعًا

ما كان يجب أن يخبرك به الإدخال بالفعل

يتخذ جزء المعاينة قرارات أفضل عندما يظهر الملف مع ملف بيانات مرفق بالفعل: مشفر أم لا، وجود JavaScript أو غيابه، تعداد المرفقات، نوع النموذج. تنتمي خطوة الفحص هذه إلى ما قبل العارض في المسار، والنمط في بناء منصة مراجعة إدخال PDF ينتج بالضبط العلامات التي تريد سياسة المعاينة استهلاكها. تُفتح الملفات التي تم تمييزها على أنها محفوفة بالمخاطر عند الإدخال من خلال المسار المحصن تلقائيًا؛ بينما تحتفظ المستندات الروتينية بوسائل الراحة الخاصة بها. اربط المرحلتين بكائن سياسة مشترك واحد بدلاً من شاشتي تكوين، والتي ستنحرف عن بعضها البعض بحلول الإصدار الثاني بغض النظر عن مدى العناية التي تكتبها بها في المرة الأولى

أين يقع الخط الفاصل بين داخل العملية وخارج العملية يعتمد على من يرسل لك الملفات. بالنسبة لإدخال الأعمال العادي، فإن الأشخاص الذين يرسلون المستندات معروفون ومجرد مهملين، والمعاينة داخل العملية مع إيقاف البرمجة النصية واعتراض الروابط هي عتبة يمكن الدفاع عنها. بالنسبة للتحميلات العامة المجهولة، فإن الأمر ليس كذلك، ولا يوجد مقدار من تعيين العلامات داخل العملية يجعلها كذلك؛ قم بتجسيد هذه الملفات في عملية عاملة منفصلة منخفضة الامتيازات وأرسل الصور النقطية إلى واجهة المستخدم، بحيث يكلفك الخلل في المحرك عملية عاملة بدلاً من التطبيق المضيف. حدد هذا التقسيم بشكل متعمد واكتب في أي فئة يقع كل مسار ابتلاع، لأن تكلفة التخمين الخاطئ غير متكافئة

يوجد الترخيص، وسطح واجهة برمجة التطبيقات (API) المرتبط بالأمان، وعرض توضيحي لعارض محصن على صفحة المنتج: مكون PDFium