Τεχνικό Άρθρο

Ασφαλής προεπισκόπηση PDF σε εφαρμογές Delphi με το PDFium Component

Η προεπισκόπηση ενός μη έμπιστου PDF μέσα στη δική σας εφαρμογή είναι μια απόφαση εκτέλεσης, και το κομμάτι που έχει σημασία δεν είναι το περίβλημα του viewer αλλά αυτό που το πάνελ αρνείται να κάνει από μόνο του. Μην γράφετε το αρχείο στον δίσκο. Μην αφήνετε τους συνδέσμους του να καλούν το κέλυφος. Μην δίνετε στα συνημμένα του μια διαδρομή. Η περισσότερη ζημιά από ένα εχθρικό έγγραφο δεν προέρχεται από ένα exploit της μηχανής αλλά από έναν viewer που κάνει εντελώς συνηθισμένα πράγματα με είσοδο που παρέχει ο επιτιθέμενος: ανοίγοντας έναν σύνδεσμο file:// προς ένα κοινόχρηστο UNC που διαρρέει διαπιστευτήρια NTLM, αφήνοντας ένα προσωρινό αντίγραφο στον φάκελο temp, αντιγράφοντας ενσωματωμένα payload όπου κι αν του λέει ένα string ονόματος αρχείου. Το PDFium Component είναι ένας viewer PDF με πηγαίο κώδικα για Delphi, C++Builder και Lazarus, και βάζει τους σχετικούς διακόπτες εκεί όπου μπορείτε να τους φτάσετε: μια σημαία κατά τη φόρτωση που σκοτώνει το scripting, events κλικ συνδέσμων που μπορείτε να αρνηθείτε με βέτο, πρόσβαση σε συνημμένα που περνά μέσα από τον δικό σας κώδικα, και bit δικαιωμάτων που μπορείτε να διαβάσετε. Η σειρά παρακάτω ακολουθεί ένα έγγραφο από τη στιγμή που φτάνει μέχρι τη στιγμή που ένας χρήστης κάνει κλικ σε κάτι μέσα του

Το μοντέλο απειλής ενός πάνελ προεπισκόπησης

Να είστε ειλικρινείς για το τι σας αγοράζει η "ασφαλής προεπισκόπηση". Ο renderer αναλύει μη έμπιστα bytes ό,τι κι αν κάνετε, και η δική της θωράκιση της μηχανής είναι το δάπεδο πάνω στο οποίο στέκεστε. Οτιδήποτε πάνω από εκείνο το δάπεδο είναι πολιτική εφαρμογής: αν αρχικοποιούνται scripts, τι κάνει ένα κλικ συνδέσμου, αν τα ενσωματωμένα αρχεία μπορούν να φτάσουν στον δίσκο, αν το πρόχειρο και ο εκτυπωτής είναι πόρτες ή τοίχοι. Ένα πράγμα που πρέπει να διαγράψετε νωρίς είναι ο διακόπτης FPDF_SetSandBoxPolicy της μηχανής. Οι περισσότεροι περιορισμοί της μηχανής είναι ήδη ενσωματωμένοι στη μεταγλώττιση, ο διακόπτης αλλάζει λίγα στην πράξη, και το να βασίζετε σε αυτόν οποιοδήποτε μέρος της αφήγησης απομόνωσής σας απλώς παράγει μια ψευδή αίσθηση ότι κάνατε κάτι. Όταν η είσοδος είναι γνήσια εχθρική, ας πούμε μια δημόσια πύλη μεταφόρτωσης, η μόνη πραγματική απομόνωση είναι η απόδοση σε μια ξεχωριστή διεργασία χαμηλού προνομίου και η αποστολή bitmaps στο UI. Οι σημαίες εντός διεργασίας είναι πολιτική. Δεν είναι περιορισμός

Διάγραμμα PDFium Component που αντιπαραθέτει διακόπτες πολιτικής προεπισκόπησης PDF μέσα στη διεργασία με απόδοση εκτός διεργασίας σε worker χαμηλών δικαιωμάτων για εχθρικά έγγραφα
Σημαίες εντός διεργασίας σηκώνουν τον πήχη για γνωστούς αποστολείς, ενώ ανώνυμα ανεβάσματα δικαιολογούν ξεχωριστό worker χαμηλών δικαιωμάτων που αποστέλλει μόνο bitmaps στο UI

Δύο επιφάνειες είναι εύκολο να ξεχαστούν ακριβώς επειδή κανένα κλικ δεν τις αγγίζει ποτέ. Η πρώτη είναι τα προσωρινά αρχεία. Αν το pipeline σας στάζει εισερχόμενα έγγραφα στον δίσκο πριν την προεπισκόπηση, εκείνα τα σταδιοποιημένα αντίγραφα επιζούν της συνεδρίας εκτός αν κάτι τα διαγράφει επαληθεύσιμα, και ένα αρχείο που είναι "ανακτήσιμο από τον φάκελο temp" έχει σιωπηλά νικήσει κάθε έλεγχο που επιβάλλει το ίδιο το πάνελ. Φορτώστε από τη μνήμη μέσω του TPdfStreamAdapter αντ' αυτού, ώστε τα εχθρικά bytes να μην αποκτήσουν ποτέ τη δική τους διαδρομή. Η δεύτερη είναι το πρόχειρο. Μια προεπισκόπηση που επιτρέπει επιλογή-και-αντιγραφή έχει ήδη εξάγει το έγγραφο, μια οθόνη κάθε φορά, και καμία υποκλοπή συνδέσμων δεν θα το πιάσει αυτό

Σκοτώστε τη JavaScript κατά τη φόρτωση, όχι στο UI

Η JavaScript εγγράφου στο PDFium Component αρχικοποιείται μόνο μαζί με το περιβάλλον form-fill. Η φόρτωση με FormFill := False επομένως απενεργοποιεί το scripting στη ρίζα αντί να καταστέλλει τα συμπτώματά του:

procedure TPreviewPane.LoadUntrusted(const FilePath: string);
begin
  Pdf.FileName := FilePath;
  Pdf.FormFill := False;     // κανένα περιβάλλον φόρμας, άρα καμία μηχανή JavaScript
  Pdf.Active := True;

  FPermissions := Pdf.Permissions;   // ακατέργαστη λέξη σημαιών· όλα τα bit ενεργά = απεριόριστο
end;

Ο συμβιβασμός είναι πραγματικός και ανήκει στο spec σας. Με το form fill απενεργοποιημένο, η γνήσια αλληλεπίδραση AcroForm και τα scripts επικύρωσης χάνονται επίσης· τα πεδία αποδίδονται με την τελευταία αποθηκευμένη εμφάνισή τους αλλά δεν μπορούν να επεξεργαστούν. Για ένα πάνελ προεπισκόπησης αυτή είναι συνήθως η σωστή επιλογή, αφού προεπισκόπηση σημαίνει κοίταγμα, όχι συμπλήρωση. Αλλά αν το ίδιο παράθυρο διπλασιάζει τον ρόλο του και ως επιφάνεια συμπλήρωσης φορμών για έμπιστα εσωτερικά έγγραφα, η απάντηση είναι δύο διαδρομές φόρτωσης με μια ρητή απόφαση εμπιστοσύνης ανάμεσά τους, όχι μία διαδρομή με μια ρύθμιση συμβιβασμού που είναι πολύ χαλαρή για την εχθρική περίπτωση και πολύ αυστηρή για την έμπιστη. Η πλευρά συμπλήρωσης φορμών εκείνου του διαχωρισμού έχει τις δικές της παγίδες, που καλύπτονται στο πλοήγηση πεδίων φόρμας και αναγέννηση εμφάνισης

Σύνδεσμοι: ο προεπιλεγμένος χειριστής καλεί το κέλυφος

Αφημένα μόνα τους, τα κλικ συνδέσμων πηγαίνουν κατευθείαν στο λειτουργικό σύστημα. Τα προεπιλεγμένα LinkOptions του viewer περιλαμβάνουν το loAutoOpenURI, που είναι η διαρροή file://-προς-UNC που περιμένει να συμβεί. Δύο events σχηματίζουν το σημείο συμφόρησης: το OnWebLinkClick για URL που εντοπίζονται σε κείμενο σελίδας, και το OnAnnotationLinkClick για annotation συνδέσμων που φέρουν ενέργειες URI ή εκκίνησης. Ορίστε Handled := True και στα δύο, ανεξαρτήτως συνθηκών, πριν αποφασίσετε οτιδήποτε, και μετά επιτρέψτε ξανά μόνο ό,τι επιτρέπει η πολιτική. Ως δεύτερο επίπεδο, αφαιρέστε το loAutoOpenURI από το LinkOptions για εχθρική είσοδο και βεβαιωθείτε ότι το loAutoLaunch, απενεργοποιημένο εξ ορισμού, δεν ξαναμπαίνει ποτέ κρυφά μέσω ενός αντιγραμμένου config:

Διάγραμμα ροής αναχαίτισης κλικ συνδέσμων PDF σε πάνελ προεπισκόπησης Delphi με έλεγχο προθέματος σχήματος ακατέργαστης συμβολοσειράς και καταγραφή ελέγχου μπλοκαρισμένων συνδέσμων
Ο ορισμός Handled και στα δύο γεγονότα συνδέσμου κρατά κάθε κλικ υπό πολιτική εφαρμογής, και ένας έλεγχος προθέματος πάνω στην ωμή συμβολοσειρά κρατά τα σχήματα file:// και UNC έξω
procedure TPreviewPane.PdfViewWebLinkClick(Sender: TObject;
  const Url: WString; var Handled: Boolean);
begin
  Handled := True;   // ποτέ μην περάσετε στην προεπιλεγμένη συμπεριφορά κελύφους

  if (AnsiStartsText('https://', Url) or AnsiStartsText('http://', Url))
    and HostIsAllowed(Url) then
    OpenInBrowser(Url)
  else
    FAudit.LogBlockedLink(FDocumentId, Url);
end;

Δύο λεπτομέρειες αποφασίζουν αν αυτό πράγματι κρατά. Πρώτον, ο έλεγχος σχήματος πρέπει να είναι έλεγχος προθέματος στο ακατέργαστο string πριν από οποιαδήποτε ανάλυση, επειδή το file://, οι διαδρομές UNC, και τα εξωτικά σχήματα είναι ακριβώς οι τιμές που καταρρίπτουν έναν αφελή αναλυτή URL ή γλιστρούν μέσα από έναν που κανονικοποιεί υπερβολικά πρόθυμα. Δεύτερον, καταγράψτε κάθε αποκλεισμό με την ταυτότητα του εγγράφου συνημμένη. Μια χούφτα αποκλεισμένων συνδέσμων file:// είναι θόρυβος φόντου· μια έκρηξη τους σε πολλά εισερχόμενα έγγραφα σε μικρό χρονικό παράθυρο είναι ένα περιστατικό που η ομάδα ασφαλείας σας θα προτιμούσε να ακούσει από εσάς παρά από κάπου αλλού

Συνημμένα: πολιτική επεκτάσεων και το όνομα αρχείου που δεν επιλέξατε

Ένα PDF είναι δοχείο, και το AttachmentCount με την ιδιότητα AttachmentName[] σας λέει τι μεταφέρει πριν οτιδήποτε αγγίξει τον δίσκο. Δύο ξεχωριστοί έλεγχοι έχουν σημασία εδώ, και μόνο ο ένας από αυτούς είναι προφανής. Ο προφανής είναι η πολιτική τύπου: μια λίστα επιτρεπόμενων επεκτάσεων που επιτρέπεται ποτέ να εξαχθούν. Η λεπτή είναι ότι το όνομα του συνημμένου είναι δεδομένα ελεγχόμενα από τον επιτιθέμενο, τελεία. Ένα ενσωματωμένο όνομα όπως ..\..\Startup\update.exe μετατρέπει μια απρόσεκτη αποθήκευση σε διάσχιση διαδρομής που ρίχνει ένα εκτελέσιμο σε έναν φάκελο που τα Windows τρέχουν κατά τη σύνδεση. Το component σας δίνει το payload ως bytes μέσω του Attachment[] και αφήνει τον κώδικά σας να επιλέξει τη διαδρομή, οπότε χτίστε αυτή τη διαδρομή από ένα εξυγιασμένο βασικό όνομα και ποτέ από το ακατέργαστο ενσωματωμένο string:

Διάγραμμα αγωγού PDFium Component που απολυμαίνει όνομα συνημμένου PDF ελεγχόμενο από επιτιθέμενο μέσω ExtractFileName και allowlist επεκτάσεων πριν τη γραφή bytes
Το όνομα ενσωματωμένου συνημμένου είναι είσοδος επιτιθέμενου, οπότε η διαδρομή εξαγωγής ξαναχτίζεται από απολυμασμένο basename και πυλώνεται με λίστα επιτρεπόμενων επεκτάσεων fail-closed
procedure TPreviewPane.ExportAttachment(Index: Integer; const TargetDir: string);
var
  RawName, SafeName, Ext: string;
  Data: TBytes;
begin
  RawName := string(Pdf.AttachmentName[Index]);
  SafeName := ExtractFileName(RawName);    // αφαιρεί τυχόν στοιχεία διαδρομής
  Ext := LowerCase(ExtractFileExt(SafeName));

  if not FAllowedExt.Contains(Ext) then    // λίστα επιτρεπόμενων, όχι λίστα αποκλεισμού
    raise EPreviewPolicy.CreateFmt('Attachment type %s blocked by policy', [Ext]);

  Data := Pdf.Attachment[Index];           // ενσωματωμένο payload ως ακατέργαστα bytes
  TFile.WriteAllBytes(
    IncludeTrailingPathDelimiter(TargetDir) + SafeName, Data);
end;

Προτιμήστε την κατεύθυνση της λίστας επιτρεπόμενων. Μια λίστα αποκλεισμού "επικίνδυνων" επεκτάσεων είναι μια κούρσα που χάνετε την ημέρα που κάποιος οπλοποιεί μια επέκταση που δεν είχατε ξανακούσει· μια λίστα επιτρεπόμενων με .pdf, .png, και .csv αποτυγχάνει κλειστά

Τι πραγματικά υπόσχονται τα δικαιώματα κρυπτογράφησης

Ο τυπικός χειριστής ασφαλείας του ISO 32000-1 κωδικοποιεί σημαίες δικαιωμάτων για εκτύπωση, αντιγραφή περιεχομένου, και τροποποίηση, και οι ιδιότητες Permissions και UserPermissions τις εμφανίζουν ως ακατέργαστες μάσκες bit μόλις ανοίξει το έγγραφο. Ο Πίνακας 22 του ISO 32000-1 ορίζει τα bit, και ένα μη κρυπτογραφημένο αρχείο αναφέρει κάθε bit ενεργό. Διαβάστε τα και τηρήστε τα στο επίπεδο εντολών σας, αλλά να είστε σαφείς για το τι είναι. Για ένα έγγραφο κρυπτογραφημένο με κωδικό ιδιοκτήτη και κενό κωδικό χρήστη, το περιεχόμενο αποκρυπτογραφείται πλήρως κατά το άνοιγμα, και οι σημαίες είναι ένα αίτημα προς συμμορφούμενους viewer, όχι έναν μηχανισμό επιβολής. Αυτό έχει δύο συνέπειες, και τραβούν σε αντίθετες κατευθύνσεις. Ποτέ μην παρουσιάζετε σημαίες δικαιωμάτων στους χρήστες ως ιδιότητα ασφαλείας των εγγράφων που λαμβάνουν, επειδή δεν είναι. Ταυτόχρονα, τηρήστε το bit εξαγωγής προσβασιμότητας (bit 10) ακόμη και όπου η γενική αντιγραφή (bit 5) απαγορεύεται· η πρόσβαση αναγνώστη οθόνης εξαιρείται ξεχωριστά στο μοντέλο δικαιωμάτων σκόπιμα, και η αφαίρεσή της επειδή "η αντιγραφή είναι κλειστή" σπάει τη βοηθητική τεχνολογία χωρίς κανένα όφελος ασφαλείας

Επιβάλετε απαγορευμένες ενέργειες στο επίπεδο εντολών, όχι κρύβοντας κουμπιά γραμμής εργαλείων. Το Ctrl+C, τα μενού περιβάλλοντος, και η επιλογή με σύρσιμο παρακάμπτουν όλα μια γραμμή εργαλείων· ένας μόνο έλεγχος δικαιώματος μέσα στην εντολή αντιγραφής δεν παρακάμπτει τίποτα

Για έγγραφα που πράγματι απαιτούν κωδικό χρήστη, αναθέστε το Password πριν το Active := True και μεταχειριστείτε την τιμή όπως το μυστικό που είναι: ανακτήστε το από το αποθετήριο διαπιστευτηρίων σας ανά συνεδρία, κρατήστε το μακριά από logs και αναφορές κατάρρευσης, και ποτέ μην το διατηρείτε δίπλα στο έγγραφο. Ένα πάνελ προεπισκόπησης που κάνει cache κωδικούς "για ευκολία" έχει σιωπηλά γίνει μια βάση δεδομένων κωδικών χωρίς καμία από τις προστασίες μιας τέτοιας

Η εκτύπωση αξίζει τη δική της απόφαση αντί να κληρονομεί ό,τι κατέληξε ο κανόνας αντιγραφής. Μια φυσική εκτύπωση είναι εξ ορισμού μη ελεγμένη, ωστόσο ο πλήρης αποκλεισμός της εκτύπωσης τείνει να σπρώχνει τους χρήστες προς screenshots, που είναι χειρότερα σε κάθε άξονα. Ένα κοινό μέσο έδαφος είναι να επιτρέπεται η εκτύπωση αλλά να σφραγίζεται κάθε σελίδα με την ταυτότητα του χρήστη και μια χρονοσφραγίδα, επιβαλλόμενη μέσα στην εντολή εκτύπωσης. Απλώς κρατήστε τη σωστή προσδοκία γι' αυτό: ένα υδατόγραφημα είναι αποτροπή και απόδοση ευθύνης. Δεν είναι πρόληψη

Τι θα έπρεπε να σας έχει ήδη πει η εισαγωγή

Ένα πάνελ προεπισκόπησης παίρνει καλύτερες αποφάσεις όταν το αρχείο εμφανίζεται με έναν φάκελο ήδη επισυναπτόμενο: κρυπτογραφημένο ή όχι, JavaScript παρόν ή απόν, μια απογραφή συνημμένων, τον τύπο φόρμας. Εκείνο το πέρασμα επιθεώρησης ανήκει πριν τον viewer, και το πρότυπο στο χτίζοντας ένα workbench ελέγχου εισαγωγής PDF παράγει ακριβώς τις σημαίες που θέλει να καταναλώσει μια πολιτική προεπισκόπησης. Αρχεία που η εισαγωγή σημείωσε ως επικίνδυνα ανοίγουν αυτόματα μέσω της θωρακισμένης διαδρομής· τα συνηθισμένα έγγραφα κρατούν τις ευκολίες τους. Συνδέστε τα δύο στάδια σε ένα κοινόχρηστο αντικείμενο πολιτικής αντί για δύο οθόνες ρυθμίσεων, που θα αποκλίνουν μέχρι τη δεύτερη έκδοση όσο προσεκτικά κι αν τις γράψετε την πρώτη φορά

Πού πέφτει η γραμμή ανάμεσα σε εντός-διεργασίας και εκτός-διεργασίας εξαρτάται από το ποιος σας στέλνει αρχεία. Για συνηθισμένη επιχειρηματική εισαγωγή, οι άνθρωποι που στέλνουν έγγραφα είναι γνωστοί και απλώς απρόσεκτοι, και η προεπισκόπηση εντός διεργασίας με scripting απενεργοποιημένο και συνδέσμους υποκλαπτόμενους είναι ένας υπερασπίσιμος πήχης. Για ανώνυμες δημόσιες μεταφορτώσεις δεν είναι, και καμία ποσότητα ρύθμισης σημαιών εντός διεργασίας δεν το κάνει έτσι· αποδώστε αυτά σε έναν ξεχωριστό worker χαμηλού προνομίου και στείλτε bitmaps στο UI, ώστε ένα ελάττωμα της μηχανής να σας κοστίζει έναν worker και όχι την εφαρμογή υποδοχής. Αποφασίστε εκείνον τον διαχωρισμό σκόπιμα και καταγράψτε σε ποιον κάδο ανήκει κάθε διαδρομή εισαγωγής, επειδή το κόστος μιας λανθασμένης εικασίας είναι ασύμμετρο

Η αδειοδότηση, η επιφάνεια API σχετική με την ασφάλεια, και ένα demo θωρακισμένου viewer βρίσκονται στη σελίδα προϊόντος: PDFium Component