Artículo técnico

Auditar riesgos de seguridad de PDF con PDFium en Delphi

Un PDF no es solo papel. Es un contenedor que puede transportar scripts que se ejecutan al abrir el archivo, enlaces que inician programas externos, enlaces que acceden a servidores web, archivos anidados dentro de otros y una firma que declara que el documento no ha cambiado desde que alguien lo respaldó. Cuando llega un archivo de una fuente que usted no controla, la primera medida más segura no es renderizarlo, sino leer lo que el archivo declara sobre sí mismo y construir un inventario de todo lo que podría intentar hacer, para que una persona pueda decidir si realmente debe incorporarse a su flujo de trabajo

Este artículo recorre un análisis de auditoría estático 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 conllevan algún comportamiento y genera un reporte simple. Es la diferencia entre pedirle a un extraño que vacíe sus bolsillos al llegar y confiar en él simplemente porque sonrió

Qué es una auditoría y qué no lo es

Defina claramente los límites. Una vista previa en un entorno aislado (sandbox) renderiza un archivo bajo restricciones estrictas para que el usuario pueda verlo sin que afecte al resto de la máquina. Una auditoría va un paso antes: 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 bajo qué nivel, y qué elementos contiene adjuntos). Usted la ejecuta cuando un documento cruza un límite de confianza (al recibirlo por correo electrónico, en un flujo de un socio comercial) antes de que cualquier proceso posterior lo abra de manera real

El componente carga el documento para una auditoría de la misma manera que para cualquier otra tarea. Establece el nombre del archivo y lo activa, lo cual analiza la tabla de referencias cruzadas y el catálogo del documento sin renderizar una sola página. Todo lo que se describe a continuación se lee a partir de ese estado cargado y libre de 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 del 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 asociados a ninguna página o campo sino al propio documento, almacenados en el árbol /Names bajo la entrada /JavaScript. Un visor conforme los ejecuta al abrir el archivo. Ese es el mecanismo detrás de una larga lista de malware en formato PDF, debido a que permite que un archivo ejecute código en el instante en que el usuario hace doble clic, antes de haber leído una sola palabra

Un inspector necesita dos datos sobre cada uno de estos scripts: que existe y qué contiene. El componente expone el conteo 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 dice nada, pero su código podría llamar a app.launchURL o ensamblar una cadena de texto y enviarla a algún sitio no deseado. 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 sin scripts de documento no es seguro por defecto, ya que también existen scripts de páginas y campos; sin embargo, un archivo con scripts de documento siempre merece una segunda revisión. El conteo de presencia es en sí mismo un filtro útil, y el cuerpo del código es lo que permite emitir un juicio definitivo

Acciones de inicio y de URI

El siguiente comportamiento a inventariar reside en los enlaces y anotaciones. Dos tipos de acciones son las que más importan a un auditor. Una acción de inicio (Launch) arranca un programa externo o abre un archivo local al activarse el enlace. Una acción de URI abre un destino web. Un revisor que analice un documento sospechoso debería poder ver, sin hacer clic en nada, si un botón en la página tres está programado para ejecutar cmd.exe o para abrir una URL que no coincide con la marca que se muestra en la página

El componente clasifica los enlaces que encuentra y expone el tipo de acción y la ruta de destino de cada uno, de modo que una auditoría pueda listar cada acción de inicio y de URI con su destino. Esto es un reporte, no una ejecución: el auditor lee la acción de la estructura y la registra, nunca la ejecuta

El visor control que renderiza los documentos es el lugar donde ocurriría la ejecución de una acción, y su comportamiento predeterminado es deliberadamente cauteloso. El control TPdfView tiene un conjunto LinkOptions que define 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 URLs web pueden abrirse, pero loAutoLaunch está ausente, por lo que las acciones de inicio nunca se ejecutan automáticamente. Para un flujo de trabajo de auditoría, se va un paso 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 motivo para evitar el inicio por defecto es sencillo. Un salto dentro del documento es inofensivo y una URL es visible y cancelable, pero iniciar un programa externo arbitrario a partir de un clic es la acción más peligrosa que puede solicitar un enlace PDF, por lo que está desactivada a menos que usted la active de forma explícita. 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 la situación. Una firma simple certifica los bytes en el momento de la firma. Una firma de certificación, el tipo creado con una regla de detección y prevención de modificaciones de documentos, va más allá: declara qué puede cambiar de forma legítima después de que el documento fue certificado, y un visor conforme advierte si se ha modificado algo fuera de lo 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 debe estar

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 rompe la certificación). Un nivel de 2 permite completar formularios y firmar, el caso habitual para un contrato que se debe llenar y firmar pero no alterar de otra forma. Un nivel de 3 permite además anotaciones sobre el llenado de formularios y firmas. 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 esa contradicción vale la pena marcarla

El componente lee el conteo de firmas y expone cada una como un registro cuyo campo Permission contiene ese valor MDP, obtenido directamente de 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 reportar

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 este paso; verificar la cadena de certificados es un proceso independiente. Lo que reporta es la intención declarada: este archivo indica 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 coherentes con la forma en que el autor selló el documento

El resto de la superficie: archivos adjuntos y XFA

Otros dos elementos completan un inventario integral. Los archivos incrustados son documentos completos transportados dentro del PDF como adjuntos, y representan un medio de distribución clásico, debido a que un informe de apariencia inofensiva puede incluir un ejecutable o un segundo PDF malicioso en su árbol de adjuntos. El componente expone el conteo de adjuntos y el nombre de cada uno, para que la auditoría pueda listar lo que se incluye sin necesidad de extraer ni abrir nada

La presencia de XFA es la otra bandera. Un formulario XFA reemplaza al AcroForm estático con una arquitectura de formulario basada en XML que aporta su propio modelo de renderizado y scripting, una superficie más amplia y compleja que la de un formulario básico. No es necesario procesar el XFA para registrar que está allí; su sola presencia es una señal de que el archivo contiene una capa interactiva más compleja que merece una revisión detallada. El componente la reporta como un valor booleano simple

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 escribe un reporte

Al unir todas las piezas, la auditoría se resume en un único procedimiento que carga un documento, enumera sus scripts y sus cuerpos, lista sus destinos de inicio y de URI, reporta el nivel MDP de la firma, registra los adjuntos y el XFA, y escribe los hallazgos en una bitácora. No renderiza nada, por lo que es rápido y no puede ser engañado para mostrar contenido hostil en la página. La salida es un registro lineal legible por humanos sobre el cual un revisor o una regla posterior pueden actuar

El diseño que funciona bien en la práctica consiste en recopilar cada hallazgo como una línea, anteponer los que representan un riesgo real para que se sitúen al principio de la cola de revisión y guardar el reporte completo junto al archivo. Un documento sin scripts, sin acciones de inicio, sin adjuntos, sin XFA y con una certificación coherente o sin firma pasa la revisión sin problemas. Un documento que activa varias alarmas a la vez es el que una persona debería revisar antes de abrirlo en un paso posterior. La auditoría no toma la decisión de confianza por usted: se asegura de que la decisión se tome con información en lugar de a ciegas

Una vez que el archivo supera la auditoría y necesita revisarlo, hágalo bajo restricciones en lugar de en un visor por defecto. El enfoque en nuestra guía sobre construir una vista previa de PDF segura en Delphi muestra cómo evitar que el manejo automático de enlaces y el contenido activo funcionen durante una revisión controlada. Para integrar esta enumeración en un flujo de entrada completo con herramientas de revisión, consulte el artículo sobre el espacio de trabajo de revisión y recepción de PDF. Ambos se basan en el mismo principio de solo lectura y libre de renderizado, y se distribuyen como parte del PDFium Component para Delphi y C++Builder, junto con las APIs de renderizado, texto, formularios y firmas descritas en otras seccions de este blog