Articolo tecnico

Preview PDF sicura nelle applicazioni Delphi con PDFium Component

Un team service-desk che supportavo visualizzava allegati cliente in un pannello PDF incorporato. Una "fattura" costruita ad arte conteneva un link il cui testo visibile diceva https://portal.example.com, ma la cui azione puntava a un URI file:// su una share UNC controllata dall'attaccante, il tipo di destinazione che Windows risolve offrendo credenziali NTLM prima ancora che un browser si apra. Il pannello eseguì diligentemente lo shell-out al clic. Nessun exploit, nessun file malformato, nessun bug del motore: solo un viewer che faceva cose predefinite con input ostile. Un pannello di preview dentro un'applicazione line-of-business è una decisione di esecuzione, e PDFium Component, viewer PDF con sorgente per Delphi, C++Builder e Lazarus, mette nelle tue mani gli hook di policy per quella decisione: switch al load time, evento di intercettazione link, chiamate di accesso agli allegati e query sui permessi. Questo articolo percorre la superficie d'attacco nell'ordine in cui un documento la raggiunge.

Il threat model di un pannello preview

Sii onesto su cosa significhi "preview sicura". Il renderer stesso analizza byte non fidati, e l'hardening del motore è il pavimento su cui stai, ma tutto sopra quel pavimento è policy applicativa: se gli script si inizializzano, cosa accade quando un utente clicca un link, se i file incorporati possono arrivare a disco, se clipboard e stampante sono porte o muri. Una nota di scope subito: lo switch FPDF_SetSandBoxPolicy del motore ha effetto pratico minimo perché la maggior parte delle restrizioni del motore è incorporata, quindi non assegnare a esso nessuna parte della tua storia di isolamento. Per flussi di input davvero ostili, ad esempio un portale upload pubblico, isolamento reale significa renderizzare in un processo separato a basso privilegio; i flag in-process sono policy, non contenimento.

Due superfici sono facili da dimenticare perché nessun clic le tocca. File temporanei: se la pipeline parcheggia documenti in ingresso su disco prima della preview, quelle copie sopravvivono alla sessione a meno che qualcosa le elimini in modo verificabile, e "recuperabile dalla directory temp" sconfigge ogni controllo imposto dal pannello; preferisci il load da memoria tramite TPdfStreamAdapter così i byte ostili non ottengono mai un percorso proprio. E la clipboard: una preview che permette select-and-copy ha già esportato il documento, una schermata alla volta.

Uccidi JavaScript al load time, non nella UI

Il JavaScript di documento in PDFium Component si inizializza solo insieme all'ambiente form-fill. Caricare con FormFill := False disabilita quindi lo scripting alla radice invece di sopprimerne i sintomi:

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;

Il trade-off è reale e appartiene alla tua specifica: con form fill disabilitato, spariscono anche l'interazione AcroForm legittima e gli script di validazione. I campi vengono renderizzati con l'ultima apparenza salvata ma non possono essere modificati. Per un pannello preview di solito è corretto, preview significa guardare, non compilare, ma se la stessa finestra serve anche da superficie di compilazione moduli per documenti interni fidati, crea due percorsi di load con una decisione esplicita di trust tra loro, non un percorso con impostazione compromesso. Il lato form-filling di quella separazione ha le proprie trappole, coperte in navigazione dei campi modulo e rigenerazione delle apparenze.

Link: l'handler predefinito esegue shell-out

I click su link non gestiti vanno direttamente al sistema operativo: le LinkOptions predefinite del viewer includono loAutoOpenURI, che è esattamente la storia NTLM sopra. Due eventi formano il choke point: OnWebLinkClick per URL rilevati nel testo della pagina, e OnAnnotationLinkClick per annotazioni link con azioni URI o launch. In entrambi, imposta Handled := True senza condizioni, poi riabilita solo ciò che la policy consente; e, come difesa in profondità, rimuovi loAutoOpenURI da LinkOptions per input ostile e assicurati che loAutoLaunch, off per default, non rientri:

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;

Due note implementative. I controlli sullo schema devono essere prefix check sulla stringa grezza prima di qualunque parsing, perché file://, percorsi UNC e schemi esotici sono proprio i valori che fanno crashare parser URL ingenui o li aggirano. E registra ogni blocco con l'identità del documento allegata: un'ondata di link file:// bloccati su molti documenti in ingresso è un segnale di incidente che il team security vuole, non rumore.

Allegati: policy di estensione e il nome file che non hai scelto

Un PDF è un contenitore, e AttachmentCount più la proprietà AttachmentName[] ti dicono cosa trasporta prima che qualcosa tocchi il disco. Contano due controlli separati. Quello ovvio è la policy di tipo, una allowlist di estensioni che possono mai essere esportate. Quello sottile è che il nome dell'allegato è dato controllato dall'attaccante: un nome incorporato come ..\..\Startup\update.exe trasforma una save imprudente in path traversal. Il componente ti consegna il payload come byte tramite Attachment[]: il codice sceglie il percorso, quindi costruiscilo da un basename sanitizzato, mai dalla stringa incorporata grezza:

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;

Preferisci la direzione allowlist. Una blocklist di estensioni "pericolose" è una corsa che perdi il giorno in cui qualcuno arma un'estensione che non avevi mai sentito; una allowlist di .pdf, .png e .csv fallisce chiusa.

Cosa promettono davvero i permessi di cifratura

Il security handler standard di ISO 32000-1 codifica flag di permesso, stampa, copia contenuto, modifica, che le proprietà Permissions e UserPermissions espongono come bitmask grezze una volta aperto il documento; ISO 32000-1 Table 22 definisce i bit, e un file non cifrato riporta tutti i bit impostati. Leggili e rispettali nel command layer, ma comprendine la natura: per un documento cifrato con owner password e user password vuota, il contenuto si decifra completamente all'apertura, e i flag sono una richiesta ai viewer più che un meccanismo di enforcement. La conseguenza vale in entrambi i sensi. Non presentare agli utenti i flag di permesso come funzionalità di sicurezza dei documenti che inviano; e al contrario, rispetta il bit di estrazione per accessibilità, bit 10, anche dove la copia generale, bit 5, è negata: l'accesso screen-reader è separato apposta nel modello di permessi.

Applica le azioni negate al command layer, non nascondendo pulsanti toolbar. Ctrl+C, menu contestuali e drag-select bypassano tutti una toolbar; un singolo controllo di permesso dentro il comando copia non bypassa nulla.

Per documenti che richiedono una user password, assegna Password prima di Active := True e tratta il valore come il segreto che è: recuperalo dal credential store per sessione, tienilo fuori da log e crash report e non persisterlo mai accanto al documento. Un pannello preview che mette in cache password "per comodità" è diventato in silenzio un database di password senza le protezioni di uno.

La stampa merita una decisione propria invece di ereditare la regola della copia. Una stampa fisica è non auditata per definizione, ma bloccare del tutto la stampa spinge gli utenti verso screenshot, peggiori. Molti team scelgono "stampa consentita, watermark con identità utente e timestamp": applicalo dentro il comando di stampa, e ricorda che un watermark è deterrenza e attribuzione, mai prevenzione.

Cosa avrebbe già dovuto dirti l'intake

Un pannello preview decide meglio quando il file arriva con un dossier: cifrato o no, JavaScript presente, censimento allegati, tipo modulo. Quel passaggio di ispezione appartiene a monte del viewer: il pattern in costruire un workbench di intake PDF produce esattamente i flag consumati da una policy di preview. I file marcati rischiosi dall'intake possono aprirsi automaticamente tramite il percorso hardened, mentre i documenti ordinari conservano le comodità. Collega i due con un solo oggetto policy condiviso invece di due schermate configurazione che deriveranno già dalla seconda release.

Domande frequenti

Come impedisco a un PDF di eseguire JavaScript nel mio viewer Delphi?

Caricalo con FormFill := False prima di Active := True; l'ambiente scripting non si inizializza mai. Il costo: i campi AcroForm sono read-only per quella sessione.

I flag di permesso PDF bastano a impedire copia o stampa?

No. Per documenti solo owner-password i flag sono consultivi; l'enforcement avviene nel tuo command layer. Tratta la bitmask Permissions come input alla policy, non come policy.

Bloccare estensioni di allegati pericolose è sufficiente?

Usa una allowlist invece di una blocklist, sanitizza il filename incorporato con ExtractFileName prima di qualunque save, e scrivi le esportazioni solo in una directory che nessun search path o meccanismo autostart legge.

Mi serve un processo separato per visualizzare PDF non fidati in modo sicuro?

Per l'intake business ordinario, la preview in-process con scripting disabilitato e link intercettati è una soglia ragionevole. Per upload pubblici anonimi, renderizza in un processo worker separato a basso privilegio e invia bitmap alla UI: un difetto del motore ti costa un worker, non l'applicazione.

Licenza, superficie API legata alla sicurezza e demo hardened-viewer sono nella pagina prodotto: PDFium Component.