קובץ PDF אינו רק נייר. הוא מכל שיכול לשאת סקריפטים שרצים כאשר הקובץ נפתח, קישורים שמפעילים תוכנות חיצוניות, קישורים שפונים לשרתי אינטרנט, קבצים מקוננים בתוך קבצים, וחתימה הטוענת שהמסמך לא השתנה מאז שמישהו ערב לו. כאשר קובץ מגיע ממקור שאינך שולט בו, הצעד הראשון הבטוח ביותר אינו לרנדר אותו. זה לקרוא מה שהקובץ אומר על עצמו ולבנות מלאי של כל מה שהוא עלול לנסות לעשות, כדי שאדם יוכל להחליט אם הוא שייך לתזרים העבודה שלך בכלל
מאמר זה עובר על מעבר ביקורת סטטי, לקריאה בלבד, על פני שטח הסיכון ההוא באמצעות רכיב 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, מכיוון שהוא מאפשר לקובץ להריץ לוגיקה ברגע שהמשתמש לוחץ עליו פעמיים, עוד לפני שקרא מילה אחת
מבקר (auditor) רוצה שתי עובדות לגבי כל סקריפט כזה: שהוא קיים, ומה הוא מכיל. הרכיב חושף את הספירה ומאפשר לך לקרוא כל פעולה כרשומה המחזיקה את שם הסקריפט ואת הגוף המלא שלו. קריאת הגוף חשובה. סקריפט בשם 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
ההתנהגות הבאה שיש למפות חיה על קישורים ואנוטציות. שני סוגי פעולות חשובים ביותר למבקר. פעולת הפעלה (Launch action) מפעילה תוכנה חיצונית או פותחת קובץ מקומי כאשר הקישור מופעל. פעולת URI פותחת יעד אינטרנט. סוקר המביט במסמך חשוד צריך להיות מסוגל לראות, מבלי ללחוץ על דבר, שכפתור בעמוד שלוש מחובר להפעלת cmd.exe או לפתיחת URL שאינו תואם למותג שבעמוד
הרכיב מסווג את הקישורים שהוא מוצא וחושף את סוג הפעולה ונתיב היעד עבור כל אחד, כך שביקורת יכולה לפרט כל פעולת Launch ו-URI עם היעד שלה. זהו דיווח, לא הרצה. המבקר קורא את הפעולה מתוך המבנה וכותב אותה. הוא לעולם אינו עוקב אחריה
פקד המציג המרנדר מסמכים הוא המקום שבו מעקב אחר פעולה היה מתרחש, והגישה שלו כברירת מחדל היא זהירה בכוונה. לפקד TPdfView יש קבוצת LinkOptions הקובעת אילו סוגי קישורים מופעלים אוטומטית בלחיצה. ברירת המחדל שלו היא [loAutoGoto, loAutoOpenURI], מה שפירושו שקפיצות בתוך המסמך וכתובות אתרים עשויות להיפתח, אך 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 היא מספר שלם עם שלושה ערכים מוגדרים. רמה של 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
שני פריטים נוספים משלימים מלאי מלא. קבצים מוטמעים הם מסמכים שלמים הנישאים בתוך ה-PDF כקבצים מצורפים (attachments), והם כלי משלוח קלאסי, מכיוון שדוח שנראה תמים יכול לשלוח קובץ הרצה או PDF זדוני שני בעץ הקבצים המצורפים שלו. הרכיב חושף את ספירת הקבצים המצורפים ואת השם של כל קובץ מצורף, כך שהביקורת יכולה לפרט מה רוכב יחד איתו מבלי לחלץ או לפתוח דבר מזה
נוכחות XFA היא הסימון השני. טופס XFA מחליף את ה-AcroForm הסטטי בארכיטקטורת טפסים מבוססת XML המביאה מודל רינדור וסקריפטים משלה, שטח גדול ומורכב יותר מאשר טופס פשוט. אינך צריך לעבד את ה-XFA כדי לציין שהוא שם; עצם נוכחותו היא סימן שהקובץ נושא שכבה אינטראקטיבית עשירה יותר הראויה למבט מקרוב. הרכיב מדווח על כך כערך בוליאני בודד
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 Component עבור Delphi ו-C++Builder, לצד ממשקי ה-API לרינדור, טקסט, טפסים וחתימות המכוסים במקומות אחרים בבלוג זה