Un equipo de mesa de ayuda al que di soporte previsualizaba adjuntos de clientes en un panel PDF incrustado. Una "factura" construida a propósito traía un enlace cuyo texto visible decía https://portal.example.com, pero cuya acción apuntaba a un URI file:// en un recurso UNC compartido controlado por un atacante, el tipo de destino que Windows resuelve entregando credenciales NTLM antes de que siquiera se abra un navegador. El panel abrió obedientemente el shell al hacer clic. Sin exploit, sin archivo malformado, sin bug del motor: solo un visor haciendo cosas predeterminadas con entrada hostil. Un panel de vista previa dentro de una aplicación de negocio es una decisión de ejecución, y PDFium Component, un visor PDF con código fuente para Delphi, C++Builder y Lazarus, pone en sus manos los puntos de extensión de política para esa decisión: opciones de carga, un evento de interceptación de enlaces, llamadas de acceso a adjuntos y consultas de permisos. Este artículo recorre la superficie de ataque en el orden en que un documento la alcanza.
El modelo de amenazas de un panel de vista previa
Sean honestos sobre qué significa "vista previa segura". El renderizador mismo analiza bytes no confiables, y el endurecimiento del motor es el piso sobre el que están parados; pero todo lo que está encima de ese piso es política de aplicación: si los scripts se inicializan, qué ocurre cuando un usuario hace clic en un enlace, si los archivos incrustados pueden llegar al disco, si el portapapeles y la impresora son puertas o paredes. Una nota de alcance al inicio: el switch FPDF_SetSandBoxPolicy del motor tiene efecto práctico mínimo porque la mayoría de las restricciones del motor ya están incorporadas, así que no presupuesten ninguna parte de su historia de aislamiento en él. Para flujos de entrada realmente hostiles, por ejemplo un portal público de carga, el aislamiento real significa renderizar en un proceso separado de bajo privilegio; las banderas in-process son política, no contención.
Hay dos superficies fáciles de olvidar porque ningún clic las toca. Archivos temporales: si su pipeline prepara documentos entrantes en disco antes de la vista previa, esas copias preparadas sobreviven a la sesión salvo que algo las elimine de forma verificable, y "recuperable desde el directorio temporal" derrota cada control que el panel mismo aplica; prefieran cargar desde memoria mediante TPdfStreamAdapter para que los bytes hostiles nunca obtengan una ruta propia. Y el portapapeles: una vista previa que permite seleccionar y copiar ya exportó el documento, una pantalla a la vez.
Matar JavaScript en la carga, no en la UI
El JavaScript de documento en PDFium Component se inicializa solo junto con el entorno de form-fill. Cargar con FormFill := False desactiva por lo tanto 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; // no form environment, hence no JavaScript engine
Pdf.Active := True;
FPermissions := Pdf.Permissions; // raw flag word; all bits set = unrestricted
end;
El intercambio es real y pertenece a la especificación: con form fill desactivado, también desaparecen la interacción legítima con AcroForm y los scripts de validación. Los campos se renderizan con su última apariencia guardada, pero no pueden editarse. Para un panel de vista previa eso suele ser correcto, vista previa significa mirar, no llenar; pero si la misma ventana también funciona como superficie de llenado de formularios para documentos internos confiables, construyan dos rutas de carga con una decisión explícita de confianza entre ellas, no una sola ruta con una configuración de compromiso. El lado de llenado de formularios de esa división tiene sus propias trampas, cubiertas en navegación de campos de formulario y regeneración de apariencias.
Enlaces: el manejador predeterminado abre el shell
Los clics de enlace no manejados van directo al sistema operativo: las LinkOptions predeterminadas del visor incluyen loAutoOpenURI, que es exactamente la historia NTLM anterior. 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 launch. En ambos, establezcan Handled := True de forma incondicional y luego vuelvan a permitir solo lo que la política autoriza; además, como defensa en profundidad, quiten loAutoOpenURI de LinkOptions para entrada hostil y asegúrense de que loAutoLaunch, desactivado de forma predeterminada, nunca se cuele:
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 notas de implementación. Las comprobaciones de esquema deben ser comprobaciones de prefijo sobre la cadena cruda antes de cualquier parsing, porque file://, rutas UNC y esquemas exóticos son precisamente los valores que rompen parsers ingenuos de URL o se les escapan. Y registren cada bloqueo con la identidad del documento adjunta: una ráfaga de enlaces file:// bloqueados en muchos documentos entrantes es una señal de incidente que su equipo de seguridad quiere ver, no ruido.
Adjuntos: política de extensión y el nombre de archivo que ustedes no eligieron
Un PDF es un contenedor, y AttachmentCount más la propiedad AttachmentName[] les dicen qué trae antes de que algo toque el disco. Importan dos controles separados. El obvio es la política de tipo, una lista de extensiones permitidas que alguna vez pueden exportarse. El sutil es que el nombre del adjunto es dato controlado por el atacante: un nombre incrustado como ..\..\Startup\update.exe convierte un guardado descuidado en una traversal de ruta. El componente entrega el payload como bytes mediante Attachment[]; su código elige la ruta, así que constrúyanla desde un basename saneado, nunca desde la cadena incrustada cruda:
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;
Prefieran la dirección de lista permitida. Una lista de bloqueo de extensiones "peligrosas" es una carrera que pierden el día en que alguien convierte en arma una extensión que nunca habían escuchado; una lista permitida de .pdf, .png y .csv falla cerrada.
Qué prometen realmente los permisos de cifrado
El manejador de seguridad estándar de ISO 32000-1 codifica banderas de permiso, impresión, copia de contenido, modificación, que las propiedades Permissions y UserPermissions exponen como bitmasks crudos una vez que el documento se abre, e ISO 32000-1 Tabla 22 define los bits; un archivo no cifrado reporta todos los bits en uno. Léanlos y respétenlos en su capa de comandos, pero entiendan su naturaleza: para un documento cifrado con contraseña de propietario y contraseña de usuario vacía, el contenido se descifra por completo al abrir, y las banderas son una solicitud a los visores más que un mecanismo de aplicación forzosa. La consecuencia corta en ambos sentidos. No presenten las banderas de permiso a los usuarios como una función de seguridad de los documentos que envían; y, a la inversa, respeten el bit de extracción para accesibilidad, bit 10, incluso cuando se niegue la copia general, bit 5: el acceso de lectores de pantalla está separado en el modelo de permisos por una buena razón.
Apliquen acciones denegadas en la capa de comandos, no ocultando botones de la barra. Ctrl+C, menús contextuales y selección con arrastre evitan una barra de herramientas; una sola comprobación de permisos dentro del comando de copiar no deja atajos.
Para documentos que sí requieren contraseña de usuario, asignen Password antes de Active := True y traten el valor como el secreto que es: tráiganlo desde el almacén de credenciales por sesión, manténganlo fuera de logs y reportes de caída, y nunca lo persistan junto al documento. Un panel de vista previa que cachea contraseñas "por comodidad" se convirtió silenciosamente en una base de datos de contraseñas sin las protecciones de una.
La impresión merece su propia decisión en lugar de heredar la regla de copia. Una impresión física no se puede auditar por definición, pero bloquear impresión por completo empuja a los usuarios hacia capturas de pantalla, que son peores. Muchos equipos terminan en "impresión permitida, con marca de agua de identidad de usuario y timestamp": apliquen eso dentro del comando de impresión, y recuerden que una marca de agua es disuasión y atribución, nunca prevención.
Lo que intake ya debería haberles dicho
Un panel de vista previa toma mejores decisiones cuando el archivo llega con un dossier: si está cifrado o no, si hay JavaScript, censo de adjuntos, tipo de formulario. Esa pasada de inspección pertenece aguas arriba del visor; el patrón de construir una mesa de revisión de PDFs entrantes produce exactamente las banderas que consume una política de vista previa. Los archivos que la ingesta marcó como riesgosos pueden abrirse automáticamente por la ruta endurecida, mientras los documentos rutinarios conservan sus comodidades. Unan ambos con un solo objeto de política compartido en lugar de dos pantallas de configuración que se desalinearán en el segundo lanzamiento.
Preguntas frecuentes
¿Cómo evito que un PDF ejecute JavaScript en mi visor Delphi?
Cárguenlo con FormFill := False antes de Active := True; el entorno de scripting nunca se inicializa. El costo: los campos AcroForm quedan de solo lectura para esa sesión.
¿Las banderas de permiso PDF bastan para impedir copia o impresión?
No. En documentos solo con contraseña de propietario, las banderas son consultivas; la aplicación ocurre en su capa de comandos. Traten el bitmask Permissions como entrada para su política, no como la política.
¿Bloquear extensiones peligrosas de adjuntos es suficiente?
Usen una lista permitida en lugar de una lista de bloqueo, saneen el nombre incrustado con ExtractFileName antes de cualquier guardado y escriban exportaciones solo en un directorio que no lea ninguna ruta de búsqueda ni mecanismo de autoinicio.
¿Necesito un proceso separado para previsualizar PDFs no confiables con seguridad?
Para intake empresarial ordinario, una vista previa in-process con scripting desactivado y enlaces interceptados es una barra razonable. Para cargas públicas anónimas, rendericen en un proceso worker separado de bajo privilegio y envíen bitmaps a la UI; así, una falla del motor cuesta un worker, no la aplicación.
Licenciamiento, la superficie de API relacionada con seguridad y un demo de visor endurecido están en la página del producto: PDFium Component.