Technical Article

Auditoría de riesgos de seguridad PDF con PDFium en Delphi

Un PDF no es solo papel. Es un contenedor que puede llevar scripts que se ejecutan cuando se abre el archivo, enlaces que inician programas externos, enlaces que acceden a servidores web, archivos anidados dentro de archivos y una firma que afirma que el documento no ha cambiado desde que alguien lo avaló. Cuando llega un archivo de una fuente que usted no controla, la primera medida más segura no es representarlo. Consiste en leer lo que el archivo dice sobre sí mismo y construir un inventario de todo lo que podría intentar hacer, de modo que un ser humano pueda decidir si pertenece a su flujo de trabajo en absoluto.

Este artículo recorre una etapa de auditoría estática 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 llevan comportamiento y escribe un informe sencillo. Es la diferencia entre pedirle a un extraño que vacíe sus bolsillos en la puerta y confiar en él porque sonrió.

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

Tenga claro el límite. Una vista previa en espacio aislado representa un archivo bajo restricciones estrictas para que un usuario pueda verlo sin que el archivo afecte al resto de la máquina. Una auditoría viene antes de eso. Es una inspección libre de representación cuya única salida es una descripción de la superficie de amenaza: qué scripts existen, qué acciones están conectadas a los enlaces, si el archivo está firmado y con qué nivel de restricción, y qué tiene adjunto. Se ejecuta cuando un documento cruza un límite de confianza, en la recepción desde el correo electrónico, un formulario de carga o un canal de socios, antes de que cualquier etapa posterior lo abra de verdad.

El componente carga un documento de la misma manera para una auditoría que para cualquier otra cosa. Establece el nombre del archivo y lo activa, lo que analiza los datos de referencia cruzada y el catálogo del documento sin representar una sola página. Todo lo que sigue se lee desde ese estado cargado y no representado.

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 documentos en el árbol de nombres

Lo primero que se debe enumerar es el código. Un PDF puede llevar 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 al abrir. Ese es el mecanismo detrás de una larga línea de malware para PDF, porque permite que un archivo execute lógica en el instante en que un usuario hace doble clic en él, antes de que haya leído una palabra.

Un auditor desea dos hechos sobre cada uno de estos scripts: que existe y qué contiene. El componente expone el recuento y le permite leer cada acción como un registro que contiene el nombre del script y su cuerpo completo. Leer el cuerpo importa. Un script llamado Doc.0 no le dice nada, pero su texto podría llamar a app.launchURL o ensamblar una cadena y pasarla a algún lugar al que no debería ir. Extraer el código fuente para que un revisor pueda leerlo es todo 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 mirada. El recuento de presencia por sí solo es una barrera útil, y el cuerpo es lo que convierte una barrera en un juicio.

Acciones Launch y URI

El siguiente comportamiento a inventariar reside en enlaces y anotaciones. Dos tipo de acciones son las que más importan a un auditor. Una acción Launch 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 de 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 Launch y URI con su destino. Esto es informes, no ejecución. El auditor lee la acción de la estructura y la anota. Nunca la sigue.

El visor control que representa los documentos es el lugar donde ocurriría el seguimiento de una acción, y su postura predeterminada es deliberadamente cautelosa. El control TPdfView tiene 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 pueden abrirse saltos internos en el documento y URL web, 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 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 inicio por defecto es simple. 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 lo más peligroso que puede solicitar un enlace PDF, por lo que está desactivado a menos que opte por participar. Un auditor opta por no utilizar incluso los comportamientos seguros, porque el trabajo consiste en mirar, no en actuar.

El nivel de permiso MDP de firma digital

Las firmas cambian la cuestión. Una firma simple da fe de los bytes en el momento de firmar. Una firma de certificación, la creada con una regla de detección y prevención de modificaciones del documento, 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 tocado algo fuera de ese margen. Leer ese nivel de permiso le dice a un auditor si un archivo está certificado y, de ser así, qué tan bloqueado debe estar.

El permiso MDP es un entero con tres valores definidos. Un nivel de 1 significa que no se permiten cambios en absoluto; cualquier modificación rompe la certificación. Un nivel de 2 permite rellenar formularios y firmar, 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 además del llenado de formularios y firma. Conocer el nivel permite a su lógica de recepción razonar sobre 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 el recuento de firmas y expone cada una como un registro cuyo campo Permission lleva ese valor MDP, poblado directamente desde la llamada FPDFSignatureObj_GetDocMDPPermission subyacente. Un permiso de cero significa que la firma no es una firma de certificación (DocMDP), por lo que no hay bloqueo 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 aquí; verificar la cadena del certificado es una preocupación diferente. Lo que reporta es la declaración de intenciones: este archivo dice que fue bloqueado en este nivel. Ese es 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 incrustados y XFA

Dos elementos más completan un inventario completo. Los archivos incrustados son documentos enteros transportados dentro del PDF como archivos adjuntos, y son un vehículo de distribución clásico, porque un informe de apariencia inofensiva puede enviar un ejecutable o un segundo PDF malicioso en su árbol de adjuntos. El componente expone el recuento de adjuntos y el nombre de cada uno, de modo que la auditoría pueda listar lo que viaja con él sin 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 scripts, una superficie más grande y compleja que un formulario simple. No es necesario procesar el XFA para notar que está allí; su mera presencia es una señal de que el archivo lleva una capa interactiva más rica que vale la pena examinar de cerca. El componente lo reporta como un solo 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 escribe un informe

Reúna las piezas y la auditoría será un único procedimiento que cargará un documento, enumerará sus scripts y sus cuerpos, listará sus destinos Launch y URI, reportará el nivel MDP de firma, tomará nota de adjuntos y XFA, y escribirá los hallazgos en un registro. No representa nada, por lo que es económico y no puede ser engañado para mostrar contenido de página hostil. La salida es un registro plano y legible por humanos sobre el cual un revisor o una regla posterior pueden actuar.

La forma que funciona bien en la práctica es recopilar cada hallazgo como una línea, anteponer los verdaderamente riesgosos para que se clasifiquen al principio de una cola de revisión y guardar todo junto al archivo. Un documento sin scripts, sin acciones Launch, sin adjuntos, sin XFA y sin firma o con una certificación coherente pasa silenciosamente. Un documento que activa varias banderas a la vez es el que una persona debería ver 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 un archivo pasa la auditoría y usted necesita examinarlo, hágalo bajo restricciones en lugar de en un visor predeterminado. El enfoque en nuestro tutorial sobre la construcción de una vista previa segura de PDF en Delphi muestra cómo evitar que el manejo automático de enlaces y el contenido activo actúen durante una visualización controlada. Para integrar esta enumeración en un flujo completo de recepción con herramientas de revisión, consulte el artículo sobre el entorno de revisión y recepción de PDF. Ambos se basan en la misma base de solo lectura y libre de representación, y se distribuyen como parte del Componente PDFium para Delphi y C++Builder, junto con las API de representación, texto, formulario y firma cubiertas en otras partes de este blog.