Artículo técnico

Secure PDF Preview in Delphi Applications with PDFium Component

Visualizar un PDF no confiable dentro de su propia aplicación es una decisión de ejecución, y lo importante no es la apariencia del visor, sino lo que el panel se niega a hacer por sí mismo. No guarde el archivo en el disco. No permita que sus enlaces ejecuten comandos en el sistema. No asigne una ruta directa a sus archivos adjuntos. La mayor parte del daño provocado por un documento hostil no proviene de una falla del motor, sino de que el visor realiza operaciones normales con datos proporcionados por un atacante: abrir un enlace file:// hacia un recurso compartido UNC que filtra credenciales NTLM, dejar una copia temporal del archivo o copiar contenidos integrados en las rutas que indique una cadena de texto. PDFium Component es un visor PDF con código fuente para Delphi, C++Builder y Lazarus que ubica los controles necesarios a su alcance: una bandera en tiempo de carga que desactiva los scripts, eventos de clics en enlaces que puede bloquear, acceso a adjuntos a través de su propio código y bits de permisos legibles. El recorrido a continuación sigue un documento desde que llega hasta que el usuario interactúa con él

El modelo de amenazas de un panel de vista previa

Sea realista sobre lo que ofrece una "vista previa segura". El renderizador analiza bytes no confiables independientemente de lo que haga, y la robustez del propio motor es la base de su seguridad. Todo lo que esté por encima de esa base es política de la aplicación: si los scripts se inicializan, qué hace un clic en un enlace, si los archivos integrados pueden guardarse en el disco y si el portapapeles y la impresora están habilitados. Una opción que debe descartar desde el principio es el control FPDF_SetSandBoxPolicy del motor: la mayoría de las restricciones del motor están compiladas, el control cambia muy poco en la práctica y depender de él para su aislamiento solo genera una falsa sensación de seguridad. Cuando la entrada es realmente hostil (como en un portal público de carga de archivos), el único aislamiento real consiste en realizar el renderizado en un proceso independiente con pocos privilegios y enviar mapas de bits a la interfaz gráfica. Las banderas dentro del mismo proceso son políticas de uso, no aislamiento real

Dos aspectos son fáciles de olvidar debido a que ningún clic interactúa con ellos. El primero son los archivos temporales: si su flujo de trabajo guarda los documentos entrantes en el disco antes de la vista previa, esas copias guardadas sobrevivirán a la sesión a menos que se eliminen explícitamente, y un archivo "recuperable desde la carpeta temporal" anula cualquier control del panel. Cargue los archivos desde la memoria utilizando TPdfStreamAdapter para que los bytes hostiles nunca obtengan una ruta propia. El segundo es el portapapeles: una vista previa que permite seleccionar y copiar ya ha exportado el documento, pantalla por pantalla, y ninguna interceptación de enlaces detendrá eso

Desactivar JavaScript en tiempo de carga, no en la interfaz

El JavaScript del documento en el PDFium Component se inicializa únicamente junto con el entorno de llenado de formularios. Por lo tanto, cargar el archivo con FormFill := False desactiva los scripts desde el origen en lugar de mitigar sus consecuencias:

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;

La compensación es real y debe definirse en sus especificaciones. Con el llenado de formularios desactivado, la interacción legítima con AcroForm y los scripts de validación también se inhabilitan; los campos se renderizan con su última apariencia guardada pero no se pueden editar. Para un panel de vista previa, esta suele ser la decisión correcta, ya que previsualizar significa observar, no completar datos. Sin embargo, si la misma ventana sirve también para completar formularios de documentos internos de confianza, la solución es implementar dos rutas de carga con una decisión de confianza explícita entre ellas, en lugar de una única ruta con una configuración intermedia que resulte demasiado permisiva para el caso hostil y demasiado restrictiva para el confiable. La función de llenado de formularios tiene sus propias dificultades, descritas en navegación de campos de formulario y regeneración de apariencia

Enlaces: el controlador por defecto ejecuta comandos

Por defecto, los clics en los enlaces se envían directamente al sistema operativo. Las LinkOptions predeterminadas del visor include loAutoOpenURI, que es vulnerable a la filtración mediante enlaces file:// a recursos compartidos UNC. Dos eventos forman el punto de control: OnWebLinkClick para las URLs detectadas en el texto de la página y OnAnnotationLinkClick para las anotaciones de enlace que conllevan acciones de URI o de inicio. Establezca Handled := True en ambos eventos de manera incondicional antes de procesar nada, y luego permita solo lo que autorice su política. Como medida adicional, elimine loAutoOpenURI de las LinkOptions para entradas sospechosas y asegúrese de que loAutoLaunch (desactivado por defecto) nunca se reactive a través de una configuración copiada:

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;

Dos detalles determinan la efectividad de esto. Primero, la comprobación del esquema debe ser una verificación de prefijo en la cadena sin procesar antes de cualquier análisis, debido a que las rutas file://, las rutas UNC y los esquemas poco comunes son exactamente los valores que bloquean a un analizador de URLs sencillo o que superan a uno que normaliza los datos con demasiada flexibilidad. Segundo, registre cada bloqueo asociándolo a la identidad del documento: unos pocos enlaces file:// bloqueados representan ruido de fondo; un pico en muchos documentos entrantes en un intervalo corto es un incidente que su equipo de seguridad preferirá conocer a través de sus registros

Archivos adjuntos: política de extensiones y nombres de archivos externos

Un PDF es un contenedor, y AttachmentCount junto con la propiedad AttachmentName[] le informan sobre su contenido antes de que este llegue al disco. Dos controles independientes son importantes aquí, y solo uno es evidente. El evidente es la política de tipos: una lista de permitidos (allowlist) con las extensiones que se pueden exportar. El sutil es que el nombre del adjunto son datos controlados por el atacante. Un nombre incrustado como ..\..\Startup\update.exe convierte una acción de guardado descuidada en un salto de directorio (path traversal) que coloca un ejecutable en una carpeta que Windows ejecuta al iniciar sesión. El componente le entrega el contenido como bytes a través de Attachment[] y permite que su código elija la ruta, de modo que debe construir esa ruta a partir de un nombre base limpio y nunca desde la cadena de texto incrustada sin procesar:

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;

Es preferible optar por una lista de permitidos. Una lista de bloqueados (blocklist) de extensiones peligrosas es una carrera perdida cuando alguien utiliza una extensión de archivo desconocida; una lista de permitidos para .pdf, .png y .csv restringe el acceso de forma segura

Qué garantizan realmente los permisos de cifrado

El controlador de seguridad estándar de ISO 32000-1 codifica banderas de permisos para impresión, copia de contenido y modificación, y las propiedades Permissions y UserPermissions las exponen como máscaras de bits una vez que se abre el documento. La Tabla 22 de ISO 32000-1 define los bits, y un archivo no cifrado reporta todos los bits activos. Léalos y aplíquelos en su nivel de comandos, pero comprenda su función: para un documento cifrado con una contraseña de propietario y una contraseña de usuario vacía, el contenido se descifra por completo al abrirse, y las banderas son una solicitud para los visores conformes, no un mecanismo de bloqueo forzado. Esto tiene dos consecuencias opuestas: nunca presente las banderas de permisos a los usuarios como una propiedad de seguridad de los documentos que reciben, porque no lo son; al mismo tiempo, respete el bit de extracción para accesibilidad (bit 10) incluso cuando la copia general (bit 5) esté denegada (el acceso para lectores de pantalla está separado intencionalmente en el modelo de permisos, y eliminarlo porque "la copia está desactivada" daña la tecnología de asistencia sin aportar beneficios de seguridad)

Aplique la denegación de acciones a nivel de comandos, no ocultando botones de la barra de herramientas. El atajo Ctrl+C, los menús contextuales y la selección por arrastre omiten la barra de herramientas; una sola comprobación de permisos dentro del comando de copia evita cualquier omisión

Para los documentos que requieren contraseña de usuario, asigne Password antes de Active := True y trate el valor como el secreto que es: obténgalo de su almacén de credenciales por sesión, manténgalo fuera de los registros y reportes de fallos, y nunca lo guarde junto al documento. Un panel de vista previa que almacena contraseñas por conveniencia se convierte en una base de datos de contraseñas sin ninguna medida de seguridad

La impresión requiere su propia definición en lugar de heredar lo establecido para la regla de copia. Una impresión física no se puede auditar por definición, pero bloquear la impresión por completo suele llevar a los usuarios a realizar capturas de pantalla, lo cual es peor en todos los aspectos. Una solución intermedia común es permitir la impresión pero marcar cada página con la identidad del usuario y una marca de tiempo, aplicado dentro del comando de impresión. Tenga la expectativa correcta: una marca de agua es disuasión y atribución, no prevención

Lo que el proceso de recepción ya debería haber indicado

Un panel de vista previa toma mejores decisiones cuando el archivo llega con un reporte adjunto: si está cifrado o no, si contiene JavaScript, un inventario de adjuntos y el tipo de formulario. Ese paso de inspección corresponde a una etapa previa al visor, y el modelo en construir una plataforma de revisión y recepción de PDF genera exactamente las banderas que requiere una política de vista previa. Los archivos marcados como riesgosos en la recepción se abren automáticamente a través de la ruta protegida; los documentos rutinarios mantienen su funcionalidad. Vincule ambas etapas a un único objeto de política compartido en lugar de a dos pantallas de configuración, las cuales se desincronizarán en la segunda versión sin importar el cuidado que tenga al programarlas

El límite entre ejecutar el renderizado en el mismo proceso o en uno independiente depende de quién envíe los archivos. Para la recepción de documentos comerciales ordinarios (donde los remitentes son conocidos y a lo sumo descuidados), la vista previa en el mismo proceso con scripts desactivados y enlaces interceptados es una protección suficiente. Para subidas públicas anónimas no lo es, y ningún ajuste de banderas en el mismo proceso lo solucionará: renderice esos archivos en un proceso de trabajo independiente con pocos privilegios y envíe mapas de bits a la interfaz gráfica, de modo que una falla del motor afecte al proceso de trabajo y no a la aplicación principal. Defina esa división deliberadamente y registre a qué grupo pertenece cada ruta de recepción, ya que el costo de equivocarse es asimétrico

Las licencias, la interfaz API de seguridad y una demostración del visor protegido están disponibles en la página del producto: PDFium Component