Artículo técnico

Vista previa segura de PDF en Delphi con PDFium Component

Previsualizar un PDF no fiable dentro de su propia aplicación es una decisión de ejecución, y la parte que importa no es el cromo del visor sino lo que el panel se niega a hacer por sí mismo. No escriba el archivo a disco. No deje que sus enlaces deriven al shell. No dé a sus adjuntos una ruta. La mayor parte del daño de un documento hostil no viene de un exploit del motor sino de un visor haciendo cosas perfectamente ordinarias con entrada suministrada por el atacante: abrir un enlace file:// a un recurso compartido UNC que filtra credenciales NTLM, dejar una copia preparada en el directorio temporal, copiar cargas útiles incrustadas a dondequiera que una cadena de nombre de archivo le diga. PDFium Component es un visor PDF de código fuente para Delphi, C++Builder y Lazarus, y pone los interruptores relevantes donde puede alcanzarlos: una bandera de carga que mata el scripting, eventos de clic de enlace que puede vetar, acceso a adjuntos que pasa por su propio código, y bits de permiso que puede leer. El orden a continuación sigue un documento desde el momento en que aterriza hasta el momento en que un usuario hace clic en algo dentro de él

El modelo de amenaza de un panel de vista previa

Sea honesto sobre lo que "vista previa segura" le compra. El renderizador analiza bytes no fiables hagamos lo que hagamos, y el propio endurecimiento del motor es el suelo sobre el que se está. Todo por encima de ese suelo es política de aplicación: si los scripts se inicializan, qué hace un clic de enlace, si los archivos incrustados pueden alcanzar el disco, si el portapapeles y la impresora son puertas o muros. Una cosa que conviene descartar temprano es el interruptor FPDF_SetSandBoxPolicy del motor. La mayoría de las restricciones del motor están compiladas, el interruptor cambia poco en la práctica, y asignar parte de su historia de aislamiento a él solo produce un falso sentido de haber hecho algo. Cuando la entrada es genuinamente hostil, digamos un portal de carga pública, el único aislamiento real es renderizar en un proceso separado de baja privilegio y enviar mapas de bits a la interfaz. Las banderas dentro del proceso son política. No son contención

Diagrama de PDFium Component contrastando los conmutadores de política de vista previa PDF en proceso con el renderizado fuera de proceso en un trabajador de bajos privilegios para documentos hostiles
Los indicadores en proceso suben el listón para remitentes conocidos, mientras que las subidas anónimas justifican un worker separado de bajo privilegio que solo envía mapas de bits a la UI

Dos superficies son fáciles de olvidar precisamente porque ningún clic las toca nunca. La primera son los archivos temporales. Si su pipeline prepara documentos entrantes a disco antes de la vista previa, esas copias preparadas sobreviven a la sesión salvo que algo las borre de forma verificable, y un archivo que es "recuperable del directorio temporal" ha derrotado silenciosamente cada control que el propio panel hace cumplir. Cargue desde memoria a través de TPdfStreamAdapter en su lugar, de modo que los bytes hostiles nunca obtengan una ruta propia. La segunda es el portapapeles. Una vista previa que permite seleccionar-y-copiar ya ha exportado el documento, una pantalla a la vez, y ninguna intercepción de enlaces lo atrapará

Eliminar JavaScript en la carga, no en la interfaz

El JavaScript del documento en PDFium Component se inicializa solo junto con el entorno de relleno de formularios. Cargar con FormFill := False por tanto desactiva el scripting en la raíz en lugar de suprimir sus síntomas:

procedure TPreviewPane.LoadUntrusted(const FilePath: string);
begin
  Pdf.FileName := FilePath;
  Pdf.FormFill := False;     // sin entorno de formularios, por tanto sin motor JavaScript
  Pdf.Active := True;

  FPermissions := Pdf.Permissions;   // palabra de flags en bruto; todos los bits a 1 = sin restricciones
end;

La contrapartida es real y pertenece a su especificación. Con el relleno de formulario desactivado, la interacción legítima de AcroForm y los scripts de validación también se van; los campos se renderizan con su última apariencia guardada pero no se pueden editar. Para un panel de vista previa esa suele ser la decisión correcta, ya que vista previa significa mirar, no rellenar. Pero si la misma ventana sirve también como superficie de relleno de formulario para documentos internos de confianza, la respuesta son dos rutas de carga con una decisión de confianza explícita entre ellas, no una ruta con un ajuste de compromiso que es demasiado flojo para el caso hostil y demasiado estricta para el de confianza. El lado de relleno de formulario de esa división tiene sus propias trampas, cubiertas en navegación de campos de formulario y regeneración de apariencia

Enlaces: el manejador predeterminado deriva al shell

Dejados solos, los clics de enlace van directos al sistema operativo. Las LinkOptions predeterminadas del visor incluyen loAutoOpenURI, que es la fuga de file://-a-recurso-UNC esperando a ocurrir. Dos eventos forman el punto de estrangulamiento: OnWebLinkClick para URLs detectadas en el texto de la página, y OnAnnotationLinkClick para anotaciones de enlace que llevan acciones URI o de lanzamiento. Fije Handled := True en ambos, incondicionalmente, antes de decidir nada, luego repermita solo lo que la política autoriza. Como segunda capa, quite loAutoOpenURI de LinkOptions para la entrada hostil y asegúrese de que loAutoLaunch, desactivado por defecto, nunca se cuele de vuelta a través de una configuración copiada:

Diagrama de flujo de la intercepción de clics de enlaces PDF en un panel de vista previa de Delphi con una comprobación de prefijo de esquema de cadena en bruto y registro de auditoría de los enlaces bloqueados
Establecer Handled en ambos eventos de enlace mantiene cada clic bajo la política de la aplicación, y una comprobación de prefijo sobre la cadena en bruto deja fuera los esquemas file:// y UNC
procedure TPreviewPane.PdfViewWebLinkClick(Sender: TObject;
  const Url: WString; var Handled: Boolean);
begin
  Handled := True;   // nunca dejes que caiga al comportamiento por defecto del shell

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

Dos detalles deciden si esto realmente se sostiene. Primero, la comprobación de esquema tiene que ser una comprobación de prefijo sobre la cadena cruda antes de cualquier análisis, porque file://, las rutas UNC y los esquemas exóticos son precisamente los valores que hacen caer a un analizador de URL naïve o se escabullen por uno que normaliza con demasiado afán. Segundo, registre cada bloqueo con la identidad del documento adjunta. Un puñado de enlaces file:// bloqueados es ruido de fondo; una ráfaga de ellos a lo largo de muchos documentos entrantes en una ventana corta es un incidente que su equipo de seguridad preferiría escuchar de usted que de alguna otra parte

Adjuntos: política de extensión y el nombre de archivo que no eligió

Un PDF es un contenedor, y AttachmentCount con la propiedad AttachmentName[] le dice qué transporta antes de que nada toque el disco. Aquí importan dos controles separados, y solo uno es obvio. El obvio es la política de tipos: una lista de permitidos de extensiones que puedan exportarse alguna vez. El sutil es que el nombre del adjunto son datos controlados por el atacante, sin más. Un nombre incrustado como ..\..\Startup\update.exe convierte un guardado descuidado en un path traversal que deposita un ejecutable en una carpeta que Windows ejecuta al inicio de sesión. El componente le entrega la carga útil como bytes a través de Attachment[] y deja que su código elija la ruta, así que construya esa ruta a partir de un nombre base saneado y nunca de la cadena incrustada en bruto:

Diagrama del pipeline de PDFium Component saneando un nombre de adjunto PDF controlado por el atacante por ExtractFileName y una lista de permitidos de extensiones antes de escribir bytes
El nombre del adjunto incrustado es entrada de atacante, de modo que la vía de exportación se reconstruye desde un nombre base saneado y la compuerta una lista blanca de extensiones de fallo cerrado
procedure TPreviewPane.ExportAttachment(Index: Integer; const TargetDir: string);
var
  RawName, SafeName, Ext: string;
  Data: TBytes;
begin
  RawName := string(Pdf.AttachmentName[Index]);
  SafeName := ExtractFileName(RawName);    // quita los componentes de ruta
  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];           // payload incrustado como bytes en bruto
  TFile.WriteAllBytes(
    IncludeTrailingPathDelimiter(TargetDir) + SafeName, Data);
end;

Prefiera la dirección de lista de permitidos. Una lista de bloqueo de extensiones "peligrosas" es una carrera que pierde el día en que alguien convierte en arma una extensión de la que nunca oyó hablar; una lista de permitidos de .pdf, .png y .csv falla cerrada

Lo que los permisos de cifrado realmente prometen

El manejador de seguridad estándar de ISO 32000-1 codifica banderas de permiso para impresión, copia de contenido y modificación, y las propiedades Permissions y UserPermissions las exponen como máscaras de bits en bruto una vez que el documento se abre. ISO 32000-1 Tabla 22 define los bits, y un archivo sin cifrar reporta cada bit activado. Léalos y hónrelos en su capa de comandos, pero sea claro sobre lo que son. 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 abrir, y las banderas son una petición a visores conformes, no un mecanismo de aplicación. Eso tiene dos consecuencias, y tiran en direcciones opuestas. Nunca presente banderas de permiso a los usuarios como una propiedad de seguridad de los documentos que reciben, porque no lo son. Al mismo tiempo, honre el bit de extracción-de-accesibilidad (bit 10) incluso donde la copia general (bit 5) se deniega; el acceso del lector de pantalla está excluido por separado en el modelo de permisos a propósito, y eliminarlo porque "la copia está desactivada" rompe la tecnología asistiva sin ganancia de seguridad

Aplice las acciones denegadas a nivel de comando, no ocultando botones de la barra de herramientas. Ctrl+C, los menús contextuales y el arrastrar-seleccionar bordean todos una barra de herramientas; una única comprobación de permiso dentro del comando de copia no borde nada

Para los documentos que sí requieren una contraseña de usuario, asigne Password antes de Active := True y trate el valor como el secreto que es: recójalo de su almacén de credenciales por sesión, manténgalo fuera de logs e informes de fallo, y nunca lo persista junto al documento. Un panel de vista previa que cachea contraseñas "por conveniencia" se ha convertido silenciosamente en una base de datos de contraseñas sin ninguna de las protecciones de una

La impresión merece su propia decisión en lugar de heredar lo que la regla de copia dejara. Una impresión física es no auditada por definición, pero bloquear la impresión por completo tiende a empujar a los usuarios hacia las capturas de pantalla, que son peores en todos los ejes. Un terreno intermedio habitual es permitir la impresión pero estampar cada página con la identidad del usuario y una marca de tiempo, aplicado dentro del comando de impresión. Solo sostenga la expectativa correcta para ello: una marca de agua es disuasión y atribución. No es prevención

Lo que el intake ya debería haberle dicho

Un panel de vista previa toma mejores decisiones cuando el archivo aparece con un dossier ya adjunto: cifrado o no, JavaScript presente o ausente, un censo de adjuntos, el tipo de formulario. Ese pase de inspección pertenece aguas arriba del visor, y el patrón de construir un workbench de revisión de intake PDF produce exactamente las banderas que una política de vista previa quiere consumir. Los archivos que el intake marcó como riesgosos abren por la ruta endurecida automáticamente; los documentos rutinarios conservan sus comodidades. Amarre las dos etapas a un objeto de política compartido en lugar de dos pantallas de configuración, que derivarán separadas para el segundo lanzamiento por mucho que cuidadosamente las escriba la primera vez

Dónde trazar la línea entre dentro-de-proceso y fuera-de-proceso depende de quién le envía archivos. Para el intake de negocio ordinario, la gente que envía documentos es conocida y meramente descuidada, y la vista previa dentro-de-proceso con scripting desactivado y enlaces interceptados es un listón defendible. Para cargas públicas anónimas no lo es, y ninguna cantidad de fijación de banderas dentro-del-proceso lo convierte en tal; renderice esos en un worker separado de baja privilegio y envíe mapas de bits a la interfaz, de modo que un fallo del motor le cueste un worker en lugar de la aplicación host. Decida esa división deliberadamente y escriba en qué cubo cae cada ruta de ingestión, porque el costo de adivinar mal es asimétrico

Licencia, la superficie de API relacionada con seguridad y una demo de visor endurecido están en la página del producto: PDFium Component