Un PDF no es solo papel; es un contenedor que puede transportar scripts que se ejecutan cuando se abre el archivo, enlaces que inician programas externos, enlaces que acceden a servidores web, archivos anidados dentro de otros archivos y una firma que certifica que el documento no ha cambiado desde que alguien lo validó. Cuando llega un archivo de una fuente que usted no controla, la medida inicial más segura no es renderizarlo; consiste en leer lo que el archivo dice sobre sí mismo y construir un inventario de todo lo que podría intentar hacer, para que una persona decida si tiene cabida en su flujo de trabajo.
Este artículo describe una fase de auditoría estática y de solo lectura sobre esa superficie de riesgo utilizando el componente PDFium para Delphi y Lazarus. La auditoría nunca dibuja una página; analiza la estructura del documento, enumera las partes del archivo que contienen comportamientos y genera un informe simple. Es la diferencia entre pedirle a un extraño que vacíe sus bolsillos en la puerta y confiar en él solo porque sonrió.
Qué es una auditoría y qué no lo es
Sea claro sobre la frontera. Una vista previa en un entorno aislado (sandbox) renderiza un archivo bajo restricciones estrictas para que el usuario pueda verlo sin que el archivo afecte al resto del sistema. Una auditoría ocurre antes de eso: es una inspección libre de renderizado cuyo único resultado es una descripción de la superficie de amenazas: qué scripts existen, qué acciones están vinculadas a los enlaces, si el archivo está firmado y con qué nivel de seguridad, y qué archivos tiene adjuntos. Se ejecuta cuando un documento cruza un límite de confianza (al recibirlo por correo electrónico, un formulario de carga o un flujo de socios) antes de que cualquier etapa posterior lo abra realmente.
El componente carga un documento de la misma manera para una auditoría que para cualquier otra tarea. Usted define el nombre del archivo y lo activa, lo cual analiza los datos de referencia cruzada y el catálogo de documentos sin renderizar una sola página. Todo lo que se describe a continuación se lee a partir de ese estado cargado y no renderizado.
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'Incoming_Invoice.pdf';
Pdf.Active := True; // parses structure, renders nothing
// audit the loaded document here
finally
Pdf.Free;
end;
end;
JavaScript de documento en el árbol de nombres
Lo primero que se debe enumerar es el código. Un PDF puede contener JavaScript a nivel de documento: scripts que no están adjuntos a ninguna página o campo sino al documento mismo, almacenados en el árbol /Names bajo una entrada /JavaScript. Un visor conforme ejecuta estos scripts al abrir el archivo. Este es el mecanismo detrás de una larga lista de malware para PDF, ya que permite que un archivo execute lógica en el instante en que un usuario hace doble clic sobre él, antes de que haya leído una sola palabra.
Un auditor busca obtener dos datos sobre cada uno de estos scripts: que existe y qué contiene. El componente expone la cantidad y le permite leer cada acción como un registro que contiene el nombre del script y su cuerpo completo. Leer el cuerpo es importante: un script llamado Doc.0 no le dice nada, pero su texto podría llamar a app.launchURL o ensamblar una cadena de caracteres y enviarla a un lugar indebido. Extraer el código fuente para que un revisor pueda leerlo es el propósito de marcar un archivo que ejecuta código al abrirse.
var
I: Integer;
Action: TPdfJavaScriptAction;
begin
if Pdf.JavaScriptActionCount > 0 then
WriteLn('WARNING: document runs ', Pdf.JavaScriptActionCount,
' script(s) on open');
for I := 0 to Pdf.JavaScriptActionCount - 1 do
begin
Action := Pdf.JavaScriptAction[I];
WriteLn(' script \"', Action.Name, '\":');
WriteLn(Action.Script); // full body, for a human to read
end;
end;
Un archivo con cero scripts de documento no es automáticamente seguro, porque también existen scripts de página y de campo, pero un archivo con scripts de documento siempre merece una segunda revisión. La simple cantidad de elementos presentes es un filtro útil, y el cuerpo del script es lo que convierte ese filtro en una decisión de juicio.
Acciones Launch y URI
El siguiente comportamiento que se debe inventariar reside en los enlaces y anotaciones. Dos tipos de acciones son las más importantes para un auditor. Una acción Launch (lanzar) inicia un programa externo o abre un archivo local cuando se activa el enlace. Una acción URI abre un destino web. Un revisor que examine un documento sospechoso debería poder ver, sin hacer clic en nada, que un botón en la página tres está conectado para iniciar cmd.exe o para abrir una URL que no coincide con la marca oficial de la página.
El componente clasifica los enlaces que encuentra y expone el tipo de acción y la ruta de destino para cada uno, de modo que una auditoría pueda enumerar cada acción Launch y URI con su destino. Esto es reporte, no ejecución: el auditor lee la acción de la estructura y la registra, nunca la ejecuta.
El visor de control que renderiza los documentos es el lugar donde ocurriría la ejecución de una acción, y su postura predeterminada es deliberadamente cautelosa. El control TPdfView posee un conjunto LinkOptions que decide qué tipos de enlaces se activan automáticamente al hacer clic. Su valor predeterminado es [loAutoGoto, loAutoOpenURI], lo que significa que los saltos dentro del documento y las URL web pueden abrirse, pero loAutoLaunch está ausente, por lo que las acciones de lanzamiento nunca se ejecutan automáticamente. Para un flujo de trabajo de auditoría, se va más allá y se limpia el conjunto por completo, de modo que nada se active automáticamente mientras decide si confiar en el archivo.
// Audit posture for the viewer: nothing auto-runs, nothing auto-opens.
View.LinkOptions := [];
// The shipped default already withholds launch:
// default = [loAutoGoto, loAutoOpenURI]
// loAutoLaunch is NOT in the default set, so external programs
// are never started on a stray click out of the box.
El razonamiento detrás de retener el lanzamiento de forma predeterminada es simple. Un salto dentro del documento es inofensivo y una URL es visible y cancelable, pero iniciar un programa externo arbitrario desde un clic es la acción más peligrosa que un enlace PDF puede solicitar, por lo que está desactivada a menos que usted decida activarla. Un auditor opta por desactivar incluso los comportamientos seguros, porque su trabajo consiste en observar, no en actuar.
El nivel de permiso MDP de la firma digital
Las firmas cambian el panorama. Una firma simple da fe de los bytes en el momento de la firma. Una firma de certificación, aquella creada con una regla de detección y prevención de modificaciones de documentos (MDP), va más allá: declara qué puede cambiar legítimamente después de que el documento fue certificado, y un visor conforme advierte si se ha modificado algo fuera de ese margen permitido. Leer ese nivel de permiso le indica a un auditor si un archivo está certificado y, de ser así, qué tan restringido se supone que está.
El permiso MDP es un entero con tres valores definidos. Un nivel de 1 significa que no se permite ningún cambio en absoluto; cualquier modificación invalida la certificación. Un nivel de 2 permite el llenado y la firma de formularios, el caso común para un contrato que debe completarse y firmarse pero no alterarse de otro modo. Un nivel de 3 permite adicionalmente anotaciones sobre el llenado y la firma de formularios. Conocer el nivel permite que su lógica de recepción analice la intención: un documento certificado en el nivel 1 que sin embargo contiene campos de formulario o scripts se contradice a sí mismo, y vale la pena marcar esa contradicción.
El componente lee la cantidad de firmas y expone cada una como un registro cuyo campo Permission contiene ese valor MDP, alimentado directamente desde la llamada subyacente FPDFSignatureObj_GetDocMDPPermission. Un permiso de cero significa que la firma no es de certificación (DocMDP), por lo que no hay restricciones a nivel de documento que informar.
var
I: Integer;
Sig: TPdfSignature;
begin
if Pdf.SignatureCount = 0 then
WriteLn('document is not signed')
else
for I := 0 to Pdf.SignatureCount - 1 do
begin
Sig := Pdf.Signature[I];
case Sig.Permission of
1: WriteLn('certified: no changes allowed');
2: WriteLn('certified: form fill and signing allowed');
3: WriteLn('certified: form fill, signing and annotations allowed');
else
WriteLn('signed, but not a DocMDP certification');
end;
end;
end;
Una auditoría no valida la criptografía de la firma en esta etapa; verificar la cadena de certificados es un asunto independiente. Lo que reporta es la intención declarada: este archivo dice que fue bloqueado en este nivel. Ese es exactamente el contexto que un revisor necesita para juzgar si los cambios posteriores, o la mera presencia de contenido activo, son consistentes con la forma en que el autor selló el documento.
El resto de la superficie: archivos adjuntos y XFA
Los archivos incrustados son documentos completos transportados dentro del PDF como adjuntos, y representan un medio de distribución clásico, ya que un informe de apariencia inofensiva puede incluir un ejecutable o un segundo PDF malicioso en su árbol de adjuntos. El componente expone la cantidad de adjuntos y el nombre de cada uno, de modo que la auditoría pueda enumerar lo que viaja con el archivo sin necesidad de extraer o abrir nada de ello.
La presencia de XFA es la otra bandera. Un formulario XFA reemplaza al AcroForm estático con una arquitectura de formularios basada en XML que aporta su propio modelo de renderizado y scripting, una superficie más grande y compleja que la de un formulario convencional. No es necesario procesar el XFA para registrar que está ahí; su mera presencia es una señal de que el archivo contiene una capa interactiva más compleja que merece una mirada más cercana. El componente lo reporta como un único booleano.
var
I: Integer;
begin
if Pdf.XFA then
WriteLn('NOTE: document contains an XFA form layer');
if Pdf.AttachmentCount > 0 then
begin
WriteLn('embedded files: ', Pdf.AttachmentCount);
for I := 0 to Pdf.AttachmentCount - 1 do
WriteLn(' - ', Pdf.AttachmentName[I]);
end;
end;
Una rutina de solo lectura que genera un informe
Reúna las piezas y la auditoría se convertirá en un único procedimiento que carga un documento, enumera sus scripts y sus cuerpos, lista sus destinos Launch y URI, reporta el nivel MDP de la firma, registra los archivos adjuntos y XFA, y escribe los hallazgos en un registro. No renderiza nada, por lo que es de bajo costo computacional y no puede ser engañado para mostrar contenido de página hostil. El resultado es un registro plano y legible por humanos sobre el cual un revisor o una regla descendente pueden actuar.
La estructura que funciona bien en la práctica consiste en recopilar cada hallazgo como una línea, anteponer un prefijo a aquellos que representen un riesgo real para que se ordenen en la parte superior de una cola de revisión, y guardar todo junto al archivo. Un documento sin scripts, sin acciones de lanzamiento, sin adjuntos, sin XFA y sin firma o con una certificación coherente se procesa sin alertas. Un documento que activa varias banderas a la vez es el que una persona debería revisar antes de que cualquier etapa posterior lo abra. La auditoría no toma la decisión de confianza por usted; se asegura de que la decisión sea informada en lugar de ciega.
Una vez que el archivo supera la auditoría y necesita visualizarlo, hágalo bajo restricciones en lugar de usar un visor predeterminado. El enfoque en nuestro recorrido sobre cómo construir una vista previa de PDF segura en Delphi muestra cómo evitar que la gestión automática de enlaces y el contenido activo operen durante una visualización controlada. Para integrar esta enumeración en un flujo de entrada completo con herramientas de revisión, consulte el artículo sobre el banco de trabajo de revisión e ingreso de PDF. Ambos procesos se construyen sobre la misma base de solo lectura y libre de renderizado y se distribuyen como parte del PDFium Component para Delphi y C++Builder, junto con las API de renderizado, texto, formularios y firmas tratadas en otras secciones de este blog.