תצוגה מקדימה של PDF לא מהימן בתוך האפליקציה שלכם היא החלטת ביצוע, והחלק שחשוב אינו עיצוב המציג אלא מה החלונית מסרבת לעשות מעצמה. אל תכתבו את הקובץ לדיסק. אל תיתנו לקישוריו לקרוא למעטפת. אל תיתנו לקבצים המצורפים שלו נתיב. רוב הנזק ממסמך עוין בא לא מניצול של המנוע אלא ממציג שעושה דברים רגילים לחלוטין עם קלט שסיפק התוקף: פתיחת קישור file:// לשיתוף UNC המדליף אישורי NTLM, השארת עותק מבוים בתיקיית הזמני, העתקת מטענים מוטמעים לכל מקום שמחרוזת שם-קובץ אומרת לו. PDFium Component הוא מציג PDF בקוד-מקור עבור Delphi, C++Builder ו-Lazarus, והוא מציב את המתגים הרלוונטיים במקום שתוכלו להגיע אליהם: דגל זמן-טעינה שהורג סקריפטים, אירועי לחיצה-על-קישור שתוכלו לפסול, גישה לקבצים מצורפים הרצה דרך הקוד שלכם, וסיביות הרשאה שתוכלו לקרוא. הסדר שלהלן עוקב אחר מסמך מהרגע שהוא נוחת ועד הרגע שמשתמש לוחץ על משהו בו
מודל האיום של חלונית תצוגה מקדימה
היו כנים לגבי מה ש"תצוגה מקדימה מאובטחת" קונה לכם. הרסטרייזר מנתח בייטים לא מהימנים לא משנה מה תעשו, וההקשחה של המנוע עצמו היא הרצפה שעליה אתם עומדים. כל מה שמעל הרצפה הזו הוא מדיניות אפליקציה: אם סקריפטים מאותחלים, מה לחיצה על קישור עושה, אם קבצים מוטמעים יכולים להגיע לדיסק, אם הלוח וההדפסה הם דלתות או קירות. דבר אחד למחוק מוקדם הוא מתג ה-FPDF_SetSandBoxPolicy של המנוע. רוב הגבלות המנוע מהודרות פנימה, המתג משנה מעט בפועל, ותקצוב כל חלק מסיפור הבידוד שלכם אליו רק מייצר תחושת שווא של עשייה. כשהקלט עוין באמת, נניח פורטל העלאה ציבורי, הבידוד האמיתי היחיד הוא רינדור בתהליך נפרד בעל-הרשאות-נמוכות ושליחת מפות סיביות אל ממשק המשתמש. דגלים בתוך-תהליך הם מדיניות. הם אינם בלימה
שני משטחים קל לשכוח דווקא מפני ששום לחיצה אינה נוגעת בהם. הראשון הוא קבצים זמניים. אם הצינור שלכם מבים מסמכים נכנסים לדיסק לפני התצוגה המקדימה, אותם עותקים מבוימים שורדים מעבר לסשן אלא אם משהו מוחק אותם באופן ניתן-לאימות, וקובץ שהוא "ניתן לשחזור מתיקיית הזמני" הביס בשקט כל בקרה שהחלונית עצמה אוכפת. טענו מהזיכרון דרך TPdfStreamAdapter במקום, כך שהבייטים העוינים לעולם לא מקבלים נתיב משלהם. השני הוא הלוח. תצוגה מקדימה המאפשרת בחירה-והעתקה כבר ייצאה את המסמך, מסך אחד בכל פעם, ושום יירוט קישור לא יתפוס זאת
הרגו JavaScript בזמן הטעינה, לא בממשק המשתמש
JavaScript של מסמך ב-PDFium Component מאותחל רק יחד עם סביבת מילוי-הטפסים. טעינה עם 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-share המחכה לקרות. שני אירועים מהווים את נקודת החנק: OnWebLinkClick עבור כתובות URL שזוהו בטקסט הדף, ו-OnAnnotationLinkClick עבור הערות קישור הנושאות פעולות URI או הפעלה. הגדירו Handled := True בשניהם, ללא תנאי, לפני שאתם מחליטים דבר, ואז התירו מחדש רק את מה שהמדיניות מתירה. כשכבה שנייה, השמיטו loAutoOpenURI מ-LinkOptions עבור קלט עוין וודאו ש-loAutoLaunch, כבוי כברירת מחדל, לעולם אינו זוחל בחזרה דרך config מועתק:
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 חושפים אותם כמסכות סיביות גולמיות ברגע שהמסמך נפתח. ISO 32000-1 טבלה 22 מגדירה את הסיביות, וקובץ לא מוצפן מדווח על כל סיבית מוגדרת. קראו אותן וכבדו אותן בשכבת הפקודה שלכם, אך היו ברורים לגבי מהן. עבור מסמך מוצפן עם סיסמת בעלים וסיסמת משתמש ריקה, התוכן מפוענח במלואו בפתיחה, והדגלים הם בקשה למציגים תואמים, לא מנגנון אכיפה. לזה יש שתי השלכות, והן מושכות בכיוונים מנוגדים. לעולם אל תציגו דגלי הרשאה למשתמשים כתכונת אבטחה של המסמכים שהם מקבלים, מפני שאינם כזו. בו-זמנית, כבדו את סיבית חילוץ-הנגישות (סיבית 10) גם היכן שהעתקה כללית (סיבית 5) נדחית; גישת קורא-מסך מובדלת בנפרד במודל ההרשאות בכוונה, והפשטתה מפני ש"העתקה כבויה" שוברת טכנולוגיה מסייעת ללא רווח אבטחה
אכפו פעולות שנדחו ברמת הפקודה, לא על-ידי הסתרת כפתורי סרגל כלים. Ctrl+C, תפריטי הקשר ובחירה-בגרירה כולם עוקפים סרגל כלים; בדיקת הרשאה יחידה בתוך פקודת ההעתקה אינה עוקפת דבר
עבור מסמכים שאכן דורשים סיסמת משתמש, הקצו Password לפני Active := True והתייחסו לערך כמו לסוד שהוא: שלפו אותו ממאגר האישורים שלכם לכל סשן, שמרו אותו מחוץ ליומנים ולדוחות קריסה, ולעולם אל תשמרו אותו לצד המסמך. חלונית תצוגה מקדימה ששומרת סיסמאות במטמון "לנוחות" הפכה בשקט למסד נתוני סיסמאות ללא אף אחת מההגנות של כזה
הדפסה ראויה להחלטה משלה ולא לירושת מה שכלל ההעתקה נחת עליו. תדפיס פיזי אינו מבוקר בהגדרה, אך חסימת הדפסה לחלוטין נוטה לדחוף משתמשים אל צילומי מסך, שגרועים יותר בכל ציר. דרך-ביניים נפוצה היא להתיר הדפסה אך להטביע בכל דף את זהות המשתמש וחותמת זמן, נאכפת בתוך פקודת ההדפסה. רק החזיקו את הציפייה הנכונה לכך: סימן מים הוא הרתעה וייחוס. הוא אינו מניעה
מה שהקליטה הייתה צריכה כבר לומר לכם
חלונית תצוגה מקדימה מקבלת החלטות טובות יותר כשהקובץ מופיע עם תיק מצורף כבר: מוצפן או לא, JavaScript נוכח או נעדר, מפקד קבצים מצורפים, סוג הטופס. אותו מעבר בדיקה שייך במעלה הזרם מהמציג, והתבנית בבניית סביבת עבודה לבדיקת קליטת PDF מפיקה בדיוק את הדגלים שמדיניות תצוגה מקדימה רוצה לצרוך. קבצים שהקליטה סימנה כמסוכנים נפתחים דרך המסלול המוקשח אוטומטית; מסמכים שגרתיים שומרים על נוחיותם. קשרו את שני השלבים לאובייקט מדיניות משותף אחד ולא לשני מסכי תצורה, שיסטו זה מזה עד המהדורה השנייה לא משנה כמה בזהירות תכתבו אותם בפעם הראשונה
היכן נופל הקו בין בתוך-תהליך לבין מחוץ-לתהליך תלוי במי שולח לכם קבצים. עבור קליטת עסקים רגילה, האנשים השולחים מסמכים מוכרים ופשוט רשלנים, ותצוגה מקדימה בתוך-תהליך עם סקריפטים כבויים וקישורים מיורטים היא רף ניתן-להגנה. עבור העלאות ציבוריות אנונימיות זה אינו, ושום כמות של הגדרת-דגלים בתוך-תהליך אינה הופכת אותו לכזה; רנדרו את אלה בעובד נפרד בעל-הרשאות-נמוכות ושלחו מפות סיביות אל ממשק המשתמש, כך שפגם מנוע יעלה לכם עובד ולא את האפליקציה המארחת. החליטו על אותו פיצול בכוונה ורשמו לאיזה דלי כל מסלול קליטה נופל, מפני שעלות הניחוש השגוי אינה סימטרית
רישוי, משטח ה-API הקשור-לאבטחה והדגמת מציג-מוקשח נמצאים בדף המוצר: PDFium Component