مقال تقني

تدقيق مخاطر أمان PDF باستخدام PDFium في Delphi

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

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

ما هو التدقيق، وما هو ليس كذلك

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

يحمّل المكون مستنداً بنفس الطريقة للتدقيق كما هو الحال لأي شيء آخر. تقوم بتعيين اسم الملف وتنشيطه، مما يحلل بيانات الإحالة المرجعية (cross-reference) وفهرس المستند دون رسم صفحة واحدة. كل ما يلي يقرأ من تلك الحالة المُحمّلة وغير المعروضة

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'Incoming_Invoice.pdf';
    Pdf.Active := True;          // parses structure, renders nothing
    // audit the loaded document here
  finally
    Pdf.Free;
  end;
end;

JavaScript المستند في شجرة الأسماء

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

يريد المدقق حقيقتين حول كل نص برمجي من هذا القبيل: أنه موجود، وما يحتويه. يعرض المكون العدد ويتيح لك قراءة كل إجراء كسجل يحمل اسم النص البرمجي وجسمه الكامل. قراءة الجسم أمر مهم. لا يخبرك نص برمجي يسمى Doc.0 بأي شيء، ولكن نصه قد يستدعي app.launchURL أو يجمع سلسلة نصية ويمررها إلى مكان لا ينبغي أن تذهب إليه. سحب المصدر بحيث يمكن للمراجع قراءته هو بيت القصيد من الإبلاغ عن ملف يشغل كوداً عند الفتح

var
  I: Integer;
  Action: TPdfJavaScriptAction;
begin
  if Pdf.JavaScriptActionCount > 0 then
    WriteLn('WARNING: document runs ', Pdf.JavaScriptActionCount,
            ' script(s) on open');
  for I := 0 to Pdf.JavaScriptActionCount - 1 do
  begin
    Action := Pdf.JavaScriptAction[I];
    WriteLn('  script "', Action.Name, '":');
    WriteLn(Action.Script);   // full body, for a human to read
  end;
end;

الملف الذي يحتوي على صفر من نصوص المستند البرمجية ليس آمناً تلقائياً، لأن النصوص البرمجية للصفحة والحقل موجودة أيضاً، ولكن الملف الذي يحتوي على نصوص مستند برمجية يستحق دائماً نظرة ثانية. عدد التواجد وحده هو بوابة مفيدة، والجسم هو ما يحول البوابة إلى حُكم

إجراءات التشغيل و URI

السلوك التالي الذي يجب حصره يعيش في الروابط والتعليقات التوضيحية (annotations). هناك نوعان من الإجراءات يهمان المدقق أكثر من غيرهما. إجراء التشغيل (Launch) يبدأ برنامجاً خارجياً أو يفتح ملفاً محلياً عند تشغيل الرابط. إجراء URI يفتح هدفاً على الويب. يجب أن يكون المراجع الذي ينظر إلى مستند مشبوه قادراً على أن يرى، دون النقر فوق أي شيء، أن الزر الموجود في الصفحة الثالثة موصل لتشغيل cmd.exe أو لفتح عنوان URL لا يتطابق مع العلامة التجارية الموجودة على الصفحة

يصنف المكون الروابط التي يجدها ويعرض نوع الإجراء والمسار الهدف لكل منها، بحيث يمكن لعملية التدقيق إدراج كل إجراء Launch و URI مع وجهته. هذا إبلاغ، وليس تنفيذاً. يقرأ المدقق الإجراء من الهيكل ويكتبه. ولا يتبعه أبداً

عنصر تحكم العارض الذي يرسم المستندات هو المكان الذي يحدث فيه اتباع الإجراء، وموقفه الافتراضي حذر عن عمد. يحتوي عنصر التحكم TPdfView على مجموعة LinkOptions التي تقرر أنواع الروابط التي تعمل تلقائياً عند النقر. الإعداد الافتراضي الخاص به هو [loAutoGoto, loAutoOpenURI]، مما يعني أن القفزات داخل المستند وعناوين URL للويب قد تفتح، ولكن loAutoLaunch غائب، لذا لا تعمل إجراءات التشغيل تلقائياً أبداً. بالنسبة لسير عمل التدقيق، تذهب أبعد من ذلك وتمسح المجموعة تماماً، بحيث لا يعمل أي شيء تلقائياً على الإطلاق بينما لا تزال تقرر ما إذا كنت ستثق بالملف

// Audit posture for the viewer: nothing auto-runs, nothing auto-opens.
View.LinkOptions := [];

// The shipped default already withholds launch:
//   default = [loAutoGoto, loAutoOpenURI]
//   loAutoLaunch is NOT in the default set, so external programs
//   are never started on a stray click out of the box.

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

مستوى إذن MDP للتوقيع الرقمي

التوقيعات تغير السؤال. يشهد التوقيع العادي على البايتات في وقت التوقيع. توقيع الشهادة (certification signature)، وهو النوع الذي يتم إنشاؤه باستخدام قاعدة اكتشاف تعديل المستند ومنعه (MDP)، يذهب أبعد من ذلك: إنه يعلن ما قد يتغير بشكل شرعي بعد اعتماد المستند، ويحذر العارض المتوافق إذا تم لمس أي شيء خارج ذلك البدل. قراءة مستوى الإذن هذا تخبر المدقق ما إذا كان الملف معتمداً، وإذا كان الأمر كذلك، فما مدى إحكامه المقصود

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

يقرأ المكون عدد التوقيعات ويعرض كل منها كسجل يحمل حقله Permission قيمة MDP تلك، ويتم تعبئته مباشرة من استدعاء FPDFSignatureObj_GetDocMDPPermission الأساسي. الإذن بصفر يعني أن التوقيع ليس توقيع شهادة DocMDP، لذلك لا يوجد إقفال على مستوى المستند للإبلاغ عنه

var
  I: Integer;
  Sig: TPdfSignature;
begin
  if Pdf.SignatureCount = 0 then
    WriteLn('document is not signed')
  else
    for I := 0 to Pdf.SignatureCount - 1 do
    begin
      Sig := Pdf.Signature[I];
      case Sig.Permission of
        1: WriteLn('certified: no changes allowed');
        2: WriteLn('certified: form fill and signing allowed');
        3: WriteLn('certified: form fill, signing and annotations allowed');
      else
        WriteLn('signed, but not a DocMDP certification');
      end;
    end;
end;

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

بقية السطح: الملفات المضمنة و XFA

عنصران إضافيان يكملان القائمة الكاملة. الملفات المضمنة (Embedded files) هي مستندات كاملة تُحمل داخل PDF كمرفقات، وهي أداة توصيل كلاسيكية، لأن تقريراً يبدو حميداً يمكن أن يشحن ملفاً تنفيذياً أو ملف PDF ضاراً ثانياً في شجرة مرفقاته. يعرض المكون عدد المرفقات واسم كل مرفق، بحيث يمكن لعملية التدقيق إدراج ما يتم تضمينه دون استخراج أو فتح أي منه

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

var
  I: Integer;
begin
  if Pdf.XFA then
    WriteLn('NOTE: document contains an XFA form layer');

  if Pdf.AttachmentCount > 0 then
  begin
    WriteLn('embedded files: ', Pdf.AttachmentCount);
    for I := 0 to Pdf.AttachmentCount - 1 do
      WriteLn('  - ', Pdf.AttachmentName[I]);
  end;
end;

روتين واحد للقراءة فقط يكتب تقريراً

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

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

بمجرد أن يجتاز الملف التدقيق وتحتاج بالفعل إلى النظر إليه، افعل ذلك تحت قيود بدلاً من العارض الافتراضي. النهج في جولتنا التفصيلية حول بناء معاينة PDF آمنة في Delphi يوضح كيفية منع التعامل التلقائي مع الروابط والمحتوى النشط من التصرف أثناء إلقاء نظرة محكومة. لطي هذا الحصر في مسار إدخال كامل مع أدوات المراجع، راجع مقال منصة إدخال ومراجعة PDF. يعتمد كلاهما على نفس الأساس الذي لا يعرض والمخصص للقراءة فقط، ويأتي كجزء من مكون PDFium لـ Delphi و C++Builder، إلى جانب واجهات برمجة التطبيقات للرسم والنص والنماذج والتوقيع التي يتم تناولها في أماكن أخرى من هذه المدونة