Ένα PDF δεν είναι απλώς χαρτί. Είναι ένα κοντέινερ που μπορεί να μεταφέρει σενάρια (scripts) που εκτελούνται όταν ανοίγει το αρχείο, συνδέσμους που ξεκινούν εξωτερικά προγράμματα, συνδέσμους που επικοινωνούν με διακομιστές ιστού, αρχεία φωλιασμένα μέσα σε αρχεία, και μια υπογραφή που ισχυρίζεται ότι το έγγραφο δεν έχει αλλάξει από τότε που κάποιος εγγυήθηκε για αυτό. Όταν ένα αρχείο φτάνει από μια πηγή που δεν ελέγχετε, η ασφαλέστερη πρώτη κίνηση δεν είναι η απόδοσή του. Είναι η ανάγνωση του τι λέει το αρχείο για τον εαυτό του και η δημιουργία μιας απογραφής για όλα όσα θα μπορούσε να προσπαθήσει να κάνει, ώστε ένας άνθρωπος να αποφασίσει αν ανήκει στη ροή εργασίας σας.
Αυτό το άρθρο παρουσιάζει ένα στατικό, read-only πέρασμα ελέγχου πάνω σε αυτήν την επιφάνεια κινδύνου χρησιμοποιώντας το στοιχείο PDFium για Delphi και Lazarus. Ο έλεγχος δεν σχεδιάζει ποτέ σελίδα. Αναλύει τη δομή του εγγράφου, απαριθμεί τα μέρη του αρχείου που φέρουν συμπεριφορά και συντάσσει μια απλή αναφορά. Είναι η διαφορά μεταξύ του να ζητήσετε από έναν ξένο να αδειάσει τις τσέπες του στην πόρτα και του να τον εμπιστευτείτε επειδή χαμογέλασε.
Τι είναι ένας έλεγχος και τι δεν είναι
Να είστε σαφείς σχετικά με το όριο. Μια προεπισκόπηση σε sandbox (sandboxed preview) αποδίδει ένα αρχείο υπό αυστηρούς περιορισμούς, ώστε ο χρήστης να μπορεί να το δει χωρίς το αρχείο να αγγίζει το υπόλοιπο μηχάνημα. Ένας έλεγχος (audit) προηγείται αυτού. Πρόκειται για μια επιθεώρηση χωρίς απόδοση (render-free), της οποίας το μόνο αποτέλεσμα είναι μια περιγραφή της επιφάνειας απειλής: ποια σενάρια υπάρχουν, ποιες ενέργειες είναι συνδεδεμένες με συνδέσμους, εάν το αρχείο είναι υπογεγραμμένο και πόσο αυστηρά, και τι είναι επισυναπτόμενο. Τον εκτελείτε όταν ένα έγγραφο διασχίζει ένα όριο εμπιστοσύνης, κατά την παραλαβή από email, μια φόρμα μεταφόρτωσης ή μια ροή συνεργάτη, πριν οποιοδήποτε μεταγενέστερο στάδιο το ανοίξει πραγματικά.
Το στοιχείο φορτώνει ένα έγγραφο με τον ίδιο τρόπο για έναν έλεγχο όπως και για οτιδήποτε άλλο. Ορίζετε το όνομα του αρχείου και το ενεργοποιείτε, γεγονός που αναλύει τα δεδομένα παραπομπών (cross-reference data) και τον κατάλογο εγγράφου χωρίς να αποδώσει ούτε μία σελίδα. Όλα τα παρακάτω διαβάζονται από αυτήν τη φορτωμένη, μη αποδοθείσα κατάσταση.
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 εγγράφου στο name tree
Ένα 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;
Ένα αρχείο με μηδενικά σενάρια εγγράφου δεν είναι αυτόματα ασφαλές, επειδή υπάρχουν επίσης σενάρια σελίδας και πεδίου, αλλά ένα αρχείο με σενάρια εγγράφου αξίζει πάντα μια δεύτερη ματιά. Το πλήθος παρουσίας από μόνο του είναι μια χρήσιμη πύλη, και το σώμα είναι αυτό που μετατρέπει μια πύλη σε απόφαση.
Ενέργειες Launch και URI
Η επόμενη συμπεριφορά προς απογραφή ζει σε συνδέσμους και σημειώσεις (annotations). Δύο τύποι ενεργειών έχουν τη μεγαλύτερη σημασία για έναν ελεγκτή. Μια ενέργεια Launch ξεκινά ένα εξωτερικό πρόγραμμα ή ανοίγει ένα τοπικό αρχείο όταν ενεργοποιείται ο σύνδεσμος. Μια ενέργεια URI ανοίγει έναν στόχο ιστού. Ένας ελεγκτής που εξετάζει ένα ύποπτο έγγραφο θα πρέπει να είναι σε θέση να δει, χωρίς να κάνει κλικ σε τίποτα, ότι ένα κουμπί στη σελίδα τρία είναι συνδεδεμένο για να εκκινήσει το cmd.exe ή να ανοίξει μια διεύθυνση URL που δεν ταιριάζει με την επωνυμία στη σελίδα.
Το στοιχείο ταξινομεί τους συνδέσμους που βρίσκει και εκθέτει τον τύπο ενέργειας και τη διαδρομή στόχου για καθέναν, ώστε ένας έλεγχος να μπορεί να απαριθμήσει κάθε ενέργεια Launch και URI με τον προορισμό της. Αυτό αποτελεί αναφορά, όχι εκτέλεση. Ο ελεγκτής διαβάζει την ενέργεια από τη δομή και την καταγράφει. Δεν την ακολουθεί ποτέ.
Το στοιχείο ελέγχου προβολής (viewer control) που αποδίδει έγγραφα είναι το μέρος όπου θα συνέβαινε η παρακολούθηση μιας ενέργειας, και η προεπιλεγμένη του στάση είναι σκόπιμα προσεκτική. Το στοιχείο ελέγχου TPdfView έχει ένα σύνολο LinkOptions που αποφασίζει ποιοι τύποι συνδέσμων ενεργοποιούνται αυτόματα σε ένα κλικ. Η προεπιλογή του είναι [loAutoGoto, loAutoOpenURI], πράγμα που σημαίνει ότι οι μεταπηδήσεις εντός εγγράφου και οι διευθύνσεις URL ιστού μπορούν να ανοίξουν, αλλά το loAutoLaunch απουσιάζει, επομένως οι ενέργειες εκκίνησης (launch actions) δεν εκτελούνται ποτέ αυτόματα. Για μια ροή εργασίας ελέγχου, προχωράτε περισσότερο και καθαρίζετε εντελώς το σύνολο, ώστε τίποτα απολύτως να μην ενεργοποιείται αυτόματα ενώ αποφασίζετε αν θα εμπιστευτείτε το αρχείο.
// 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 της ψηφιακής υπογραφής
Οι υπογραφές αλλάζουν το ερώτημα. Μια απλή υπογραφή πιστοποιεί τα byte κατά τη στιγμή της υπογραφής. Μια υπογραφή πιστοποίησης (certification signature), του είδους που δημιουργείται με έναν κανόνα ανίχνευσης και πρόληψης τροποποίησης εγγράφου (modification detection and prevention), προχωρά περισσότερο: δηλώνει τι μπορεί νόμιμα να αλλάξει μετά την πιστοποίηση του εγγράφου, και ένα συμμορφούμενο πρόγραμμα προβολής προειδοποιεί εάν έχει αγγιχτεί κάτι εκτός αυτής της άδειας. Η ανάγνωση αυτού του επιπέδου άδειας λέει σε έναν ελεγκτή εάν ένα αρχείο είναι πιστοποιημένο και, εάν ναι, πόσο κλειδωμένο προορίζεται να είναι.
Η άδεια MDP είναι ένας ακέραιος με τρεις καθορισμένες τιμές. Ένα επίπεδο 1 σημαίνει ότι δεν επιτρέπεται καμία αλλαγή. Οποιαδήποτε τροποποίηση παραβιάζει την πιστοποίηση. Ένα επίπεδο 2 επιτρέπει τη συμπλήρωση φόρμας και την υπογραφή, η συνήθης περίπτωση για ένα συμβόλαιο που προορίζεται να συμπληρωθεί και να υπογραφεί αλλά όχι να τροποποιηθεί διαφορετικά. Ένα επίπεδο 3 επιτρέπει επιπλέον σημειώσεις (annotations) πάνω από τη συμπλήρωση φόρμας και την υπογραφή. Η γνώση του επιπέδου επιτρέπει στη λογική παραλαβής σας να συμπεράνει την πρόθεση: ένα έγγραφο πιστοποιημένο στο επίπεδο 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 ως συνημμένα (attachments), και αποτελούν κλασικό όχημα μεταφοράς, επειδή μια αναφορά που φαίνεται καλοήθης μπορεί να μεταφέρει ένα εκτελέσιμο ή ένα δεύτερο κακόβουλο PDF στο δέντρο συνημμένων της. Το στοιχείο εκθέτει το πλήθος των συνημμένων και το όνομα καθενός, ώστε ο έλεγχος να μπορεί να καταγράψει τι συνοδεύει το αρχείο χωρίς να εξαγάγει ή να ανοίξει τίποτα από αυτά.
Η παρουσία XFA είναι η άλλη σημαία. Μια φόρμα XFA αντικαθιστά τη στατική AcroForm με μια αρχιτεκτονική φόρμας βασισμένη σε XML που φέρει το δικό της μοντέλο απόδοσης και scripting, μια μεγαλύτερη και πιο πολύπλοκη επιφάνεια από μια απλή φόρμα. Δεν χρειάζεται να επεξεργαστείτε το 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;
Μια read-only ρουτίνα που συντάσσει μια αναφορά
Συνδυάστε τα κομμάτια και ο έλεγχος (audit) είναι μια ενιαία διαδικασία που φορτώνει ένα έγγραφο, απαριθμεί τα σενάριά του και τα σώματά τους, καταγράφει τους στόχους Launch και URI, αναφέρει το επίπεδο MDP της υπογραφής, σημειώνει τα συνημμένα και το XFA, και γράφει τα ευρήματα σε ένα αρχείο καταγραφής. Δεν αποδίδει τίποτα, επομένως είναι οικονομικό και δεν μπορεί να παραπλανηθεί ώστε να εμφανίσει εχθρικό περιεχόμενο σελίδας. Το αποτέλεσμα είναι ένα επίπεδο, αναγνώσιμο από τον άνθρωπο αρχείο που ένας ελεγκτής ή ένας μεταγενέστερος κανόνας μπορεί να αξιοποιήσει.
Το σχήμα που λειτουργεί καλά στην πράξη είναι η συλλογή κάθε ευρήματος ως γραμμή, η προσθήκη προθέματος στα πραγματικά επικίνδυνα ώστε να ταξινομούνται στην κορυφή μιας ουράς ελέγχου, και η διατήρηση ολόκληρου του αρχείου δίπλα στο αρχείο. Ένα έγγραφο χωρίς σενάρια, χωρίς ενέργειες εκκίνησης, χωρίς συνημμένα, χωρίς XFA, και είτε χωρίς υπογραφή είτε με μια συνεκτική πιστοποίηση περνά ήσυχα. Ένα έγγραφο που ενεργοποιεί πολλές σημαίες ταυτόχρονα είναι αυτό που πρέπει να δει ένας άνθρωπος πριν το ανοίξει οποιοδήποτε μεταγενέστερο στάδιο. Ο έλεγχος δεν λαμβάνει την απόφαση εμπιστοσύνης για εσάς. Διασφαλίζει ότι η απόφαση είναι τεκμηριωμένη και όχι τυφλή.
Μόλις ένα αρχείο περάσει τον έλεγχο και πρέπει να το δείτε, κάντε το υπό περιορισμούς και όχι σε ένα προεπιλεγμένο πρόγραμμα προβολής. Η προσέγγιση στον οδηγό μας για τη δημιουργία μιας ασφαλούς προεπισκόπησης PDF στο Delphi δείχνει πώς να αποτρέψετε την αυτόματη διαχείριση συνδέσμων και το ενεργό περιεχόμενο από το να δράσουν κατά τη διάρκεια μιας ελεγχόμενης ματιάς. Για να ενσωματώσετε αυτήν την απαρίθμηση σε μια πλήρη ροή εργασίας με εργαλεία αναθεώρησης, δείτε το άρθρο για τον πάγκο εργασίας εισαγωγής και αναθεώρησης PDF. Και τα δύο βασίζονται στην ίδια read-only, render-free βάση και αποστέλλονται ως μέρος του PDFium Component για Delphi και C++Builder, μαζί με τα API απόδοσης, κειμένου, φόρμας και υπογραφής που καλύπτονται αλλού σε αυτό το ιστολόγιο.