كان فريق service-desk دعمتُه يعاين مرفقات العملاء داخل جزء PDF مدمج. حملت "فاتورة" مصممة يدويًا رابطًا يظهر نصه https://portal.example.com لكن action الخاصة به تشير إلى URI من نوع file:// على UNC share يتحكم فيه مهاجم، وهو نوع الوجهة الذي يحله Windows عبر تقديم NTLM credentials قبل أن يفتح أي browser أصلًا. كان الجزء يطلق shell عند النقر بلا تردد. لا exploit، ولا ملف malformed، ولا عيب في engine: مجرد عارض يفعل الأشياء الافتراضية مع input عدائي. جزء preview داخل تطبيق line-of-business هو قرار تنفيذ، وPDFium Component، عارض PDF مع source code لـ Delphi وC++Builder وLazarus، يضع hooks السياسة لذلك القرار بين يديك: switches وقت التحميل، وحدث اعتراض الروابط، واستدعاءات الوصول إلى المرفقات، واستعلامات الصلاحيات. تسير هذه المقالة على attack surface بالترتيب الذي يصل فيه المستند إليها.
Threat model لجزء preview
كن صريحًا بشأن معنى "secure preview". renderer نفسه يحلل bytes غير موثوقة، وتقوية engine هي الأرضية التي تقف عليها، لكن كل ما فوق تلك الأرضية هو سياسة تطبيق: هل تبدأ scripts، وماذا يحدث عندما ينقر المستخدم رابطًا، وهل يمكن للملفات المضمنة الوصول إلى disk، وهل clipboard وprinter أبواب أم جدران. ملاحظة نطاقية في البداية: مفتاح engine المسمى FPDF_SetSandBoxPolicy له أثر عملي محدود لأن معظم قيود engine مبنية داخله، فلا تخصص له أي جزء من قصة العزل. بالنسبة إلى input عدائي فعلًا، مثل public upload portal، يعني العزل الحقيقي التصيير في عملية منفصلة منخفضة الصلاحيات؛ أما flags داخل العملية فهي سياسة، لا احتواء.
يوجد سطحان يسهل نسيانهما لأنهما لا ينتظران نقرة. temporary files: إذا كان pipeline يضع المستندات الواردة على disk قبل preview، فإن هذه النسخ المرحلية تعيش بعد الجلسة إلا إذا حذفها شيء قابل للتحقق، وعبارة "قابلة للاسترداد من temp directory" تهزم كل control يفرضه الجزء نفسه؛ فضّل التحميل من الذاكرة عبر TPdfStreamAdapter حتى لا تحصل bytes العدائية على path خاص بها. ثم clipboard: preview يسمح بالتحديد والنسخ يكون قد صدر المستند بالفعل، screenful بعد screenful.
اقتل JavaScript عند التحميل لا في UI
Document JavaScript في PDFium Component يتهيأ فقط مع form-fill environment. لذلك فإن التحميل مع FormFill := False يعطل scripting من الجذر بدل كبت أعراضه:
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;
المقايضة حقيقية ويجب أن تكون في spec: مع تعطيل form fill، تختفي أيضًا تفاعلات AcroForm المشروعة وvalidation scripts. تظهر الحقول بآخر appearance محفوظة لكنها لا تقبل التحرير. بالنسبة إلى preview pane يكون هذا غالبًا صحيحًا، فpreview يعني النظر لا التعبئة، لكن إذا كانت النافذة نفسها تعمل كسطح form-filling لمستندات داخلية موثوقة، فابن مساري تحميل مع قرار trust صريح بينهما، لا مسارًا واحدًا بإعداد compromise. جهة form-filling من ذلك الانقسام لها فخاخها الخاصة، وهي مغطاة في التنقل بين حقول النماذج وإعادة توليد appearance.
الروابط: المعالج الافتراضي يطلق shell
نقرات الروابط غير المعالجة تذهب مباشرة إلى نظام التشغيل، فخيارات العارض الافتراضية LinkOptions تتضمن loAutoOpenURI، وهذا هو بالضبط سيناريو NTLM أعلاه. يشكل حدثان choke point: OnWebLinkClick للـ URLs المكتشفة في نص الصفحة، وOnAnnotationLinkClick لتعليقات links التي تحمل URI أو launch actions. في كليهما اضبط Handled := True بلا شرط، ثم أعد السماح فقط بما تجيزه السياسة، وكدفاع متعدد الطبقات أزل loAutoOpenURI من LinkOptions للمدخلات العدائية وتأكد أن loAutoLaunch، وهو off افتراضيًا، لا يتسرب:
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;
ملاحظتان تنفيذيتان. يجب أن تكون فحوص scheme فحوص prefix على السلسلة الخام قبل أي parsing، لأن file:// وUNC paths وschemes الغريبة هي تحديدًا القيم التي تسقط URL parsers الساذجة أو تتجاوزها. وسجل كل block مع هوية المستند، فاندفاع روابط file:// محجوبة عبر مستندات واردة كثيرة إشارة incident يريدها فريق security، لا ضجيج.
المرفقات: سياسة الامتداد واسم الملف الذي لم تختره
PDF حاوية، وAttachmentCount مع الخاصية AttachmentName[] يخبرانك بما يحمله قبل أن يلمس أي شيء disk. يهم controlان منفصلان. الأول واضح وهو سياسة النوع، أي allowlist للامتدادات التي يجوز تصديرها أصلًا. الثاني أدق: اسم المرفق data يتحكم بها المهاجم؛ اسم مضمن مثل ..\..\Startup\update.exe يحول save مهملًا إلى path traversal. يعطيك component payload كـ bytes عبر Attachment[]، وشيفرتك هي التي تختار path، لذلك ابنه من basename منقح، لا من السلسلة المضمنة الخام:
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;
فضّل اتجاه allowlist. blocklist للامتدادات "الخطرة" سباق تخسره في اليوم الذي يستغل فيه شخص امتدادًا لم تسمع به؛ أما allowlist من .pdf و.png و.csv فتفشل مغلقة.
ما الذي تعد به صلاحيات encryption فعلًا
يشفر standard security handler في ISO 32000-1 permission flags للطباعة ونسخ المحتوى والتعديل، وتعرض خصائص Permissions وUserPermissions هذه القيم كـ raw bitmasks بعد فتح المستند، وجدول ISO 32000-1 Table 22 يحدد bits، بينما يبلّغ الملف غير المشفر أن كل bits مضبوطة. اقرأها واحترمها في command layer، لكن افهم طبيعتها: بالنسبة إلى مستند مشفر بـ owner password وuser password فارغ، يفك المحتوى بالكامل عند الفتح، وتصبح flags طلبًا إلى العوارض لا آلية فرض. والنتيجة تعمل في الاتجاهين. لا تعرض permission flags للمستخدمين كميزة security في المستندات التي يرسلونها؛ وفي المقابل، احترم bit استخراج accessibility، bit 10، حتى عندما يمنع النسخ العام، bit 5، فإتاحة screen reader مفصولة في permission model لسبب وجيه.
افرض actions الممنوعة في command level، لا عبر إخفاء toolbar buttons. Ctrl+C وcontext menus وdrag-select كلها تتجاوز toolbar؛ فحص صلاحية واحد داخل copy command لا يتجاوزه شيء.
بالنسبة إلى المستندات التي تحتاج user password، أسند Password قبل Active := True وتعامل مع القيمة كسر كما هي: اجلبها من credential store لكل جلسة، وأبقها خارج logs وcrash reports، ولا تحفظها أبدًا بجوار المستند. preview pane يخزن كلمات المرور "للراحة" أصبح بهدوء password database بلا أي من حماياتها.
الطباعة تستحق قرارًا مستقلًا بدل أن ترث قاعدة النسخ. النسخة المطبوعة غير قابلة للتدقيق بطبيعتها، لكن حظر الطباعة تمامًا يدفع المستخدمين إلى screenshots، وهي أسوأ. تستقر فرق كثيرة على "الطباعة مسموحة، مع watermark يحوي هوية المستخدم والطابع الزمني"، فافرض ذلك داخل print command، وتذكر أن watermark ردع وإسناد مسؤولية، لا منع.
ما الذي كان ينبغي لمرحلة intake أن تخبرك به أصلًا
يتخذ preview pane قرارات أفضل عندما يصل الملف مع dossier: هل هو encrypted، وهل يحتوي JavaScript، وجرد attachments، ونوع form. تنتمي pass الفحص هذه upstream من العارض؛ النمط في بناء PDF intake review workbench ينتج بالضبط flags التي تستهلكها preview policy. عندها يمكن فتح الملفات التي صنفتها intake كخطرة عبر المسار hardened تلقائيًا، بينما تحتفظ المستندات الروتينية بوسائل الراحة. اربط الاثنين بكائن policy مشترك واحد بدل شاشتي configuration ستنحرفان بحلول الإصدار الثاني.
الأسئلة الشائعة
كيف أوقف PDF عن تشغيل JavaScript في عارض Delphi؟
حمّله مع FormFill := False قبل Active := True؛ لن تتهيأ بيئة scripting أصلًا. الكلفة: تصبح حقول AcroForm read-only في تلك الجلسة.
هل تكفي PDF permission flags لمنع النسخ أو الطباعة؟
لا. في مستندات owner-password-only تكون flags إرشادية؛ enforcement يحدث في command layer لديك. عامل bitmask Permissions كمدخل إلى سياستك، لا كسياسة كاملة.
هل حظر امتدادات المرفقات الخطرة كاف؟
استخدم allowlist بدل blocklist، ونقح اسم الملف المضمن باستخدام ExtractFileName قبل أي save، واكتب exports فقط في directory لا تقرأ منه أي search path أو autostart mechanism.
هل أحتاج إلى عملية منفصلة لمعاينة PDFs غير موثوقة بأمان؟
بالنسبة إلى intake أعمال عادية، فإن preview داخل العملية مع تعطيل scripting واعتراض الروابط مستوى معقول. بالنسبة إلى anonymous public uploads، صير داخل worker process منخفض الصلاحيات وأرسل bitmaps إلى UI، وبذلك يكلفك خلل engine عاملًا، لا التطبيق.
تفاصيل licensing وsecurity-related API surface وhardened-viewer demo موجودة في صفحة المنتج: PDFium Component.