💡 Ejemplo completo en funcionamiento disponible en GitHub:
cargar-documentos-no-confiables-de-manera-segura-python

El método antiguo era doloroso

Escribiste tres líneas para generar una miniatura de un documento cargado. Se veían así y funcionaban bien:

with signature.Signature(upload_path) as sign:
    save_page_preview(sign, thumbnail_path)

Lo que esas líneas hacían, antes de GroupDocs.Signature 26.9, era obtener cada dirección a la que apuntaba el documento. Un archivo Word puede contener una imagen que no está dentro del archivo: el documento guarda una URL y, al abrirlo, se descarga esa URL. En un escritorio eso es una característica. En un servidor que acepta cargas, significa que la persona que te envía el archivo decide a qué direcciones solicita tu infraestructura.

El ataque tiene un nombre, server‑side request forgery (SSRF), y tres variantes que vale la pena nombrar. Una dirección interna inaccesible desde Internet es accesible desde tu servidor, de modo que un documento manipulado puede hacer que tu servicio solicite http://169.254.169.254/ o un punto final de administración en localhost. Una ruta UNC puede obligar a un host Windows a autenticarse hacia el exterior, entregando credenciales a un servidor controlado por el atacante. Y un enlace a un host que simplemente nunca responde mantiene el hilo de carga activo hasta que se agota el tiempo de espera, lo que es una forma barata de agotar un pool de workers con documentos que parecen inofensivos.

Nada de eso es un error en la biblioteca de documentos. Seguir un enlace es lo que el formato indica. Lo incómodo era que cumplir con el enlace era el comportamiento predeterminado, y en el código nadie lo señalaba en la revisión.

Existe una forma mejor

La carga segura de documentos es el comportamiento de GroupDocs.Signature para Python que rechaza esas solicitudes. A partir de la versión 26.9, LoadOptions.skip_external_resources tiene por defecto True, de modo que esas mismas tres líneas ahora no solicitan nada y renderizan un marcador de posición donde estaría la imagen enlazada.

El cambio es un valor predeterminado, no una nueva característica; la propiedad ya existía. Lo que 26.9 alteró es a qué valor apunta cuando tu código no especifica nada, que es la única configuración que la mayoría de los servicios utilizan.

La nueva forma: tres modos de carga

Paso 1 – Mantén el valor predeterminado para cualquier contenido no confiable

Sin LoadOptions en absoluto:

with signature.Signature(source_path) as sign:
    return save_page_preview(sign, preview_path)

No se solicita nada. La vista previa es más pequeña de lo que sería de otro modo, y esa diferencia de tamaño es la prueba más cómoda de que no se realizó ninguna solicitud fuera de la máquina.

Paso 2 – Lista blanca de un host que realmente poseas

Muchos documentos enlazan a algo legítimo: un CDN de la empresa, un servidor interno de imágenes, una tienda de plantillas. Permite eso y nada más:

load_options = LoadOptions()
load_options.whitelisted_resources = [trusted_address]

with signature.Signature(source_path, load_options) as sign:
    return save_page_preview(sign, preview_path)

La regla de coincidencia merece atención. Es una prueba de subcadena insensible a mayúsculas contra la dirección del recurso, lo que hace que un fragmento corto sea peligroso: github coincide con github.attacker.example/payload.png tan fácilmente como con el host que pretendías. Usa un esquema, un host y una ruta; este ejemplo incluye en la lista blanca raw.githubusercontent.com/groupdocs-signature/.

Paso 3 – Permitir todo, deliberadamente

El comportamiento pre‑26.9, todavía disponible:

load_options = LoadOptions()
load_options.skip_external_resources = False

Razonable para documentos que tu propia aplicación haya generado. Una trampa: la propiedad obsoleta load_external_resources tiene la polaridad opuesta, de modo que skip_external_resources = False reemplaza a load_external_resources = True. Copiar un valor de la propiedad antigua invierte tu postura de seguridad sin que aparezca ningún error.

Comparación lado a lado: antes vs. después

Mismo documento, mismo flujo de código, tres políticas de carga. Estos son los tamaños de los archivos comprometidos en la carpeta Result/ del ejemplo, para que puedan verificarse en lugar de confiar ciegamente:

Modo de carga Tamaño de la vista previa Solicitudes salientes
predeterminado (26.9 y posteriores) 16 435 bytes ninguna
host en lista blanca 51 738 bytes una, a la dirección permitida
todos los recursos (predeterminado pre‑26.9) 51 738 bytes una por cada recurso enlazado

La imagen enlazada representa 35 303 bytes de esa diferencia. No confié en la configuración hasta ver esos dos números uno al lado del otro, y sugiero lo mismo: leer la propiedad de vuelta te indica lo que configuraste, no lo que el proceso hizo.

¿Qué se considera un recurso externo?

Más limitado de lo que la gente espera, por eso la actualización suele ser indolora. Imágenes enlazadas en lugar de incrustadas, campos INCLUDEPICTURE, imágenes enlazadas en presentaciones y hojas de cálculo, y las imágenes y hojas de estilo que referencia un SVG. El contenido incrustado no se toca, porque ya está dentro del archivo y no se necesita ninguna solicitud para renderizarlo.

Esa distinción es todo el límite de seguridad. Un documento solo puede hacer que tu servidor salga al exterior si almacena una dirección en lugar de los bytes, así que la pregunta para cualquier corpus es simplemente cuántos de sus archivos enlazan en vez de incrustar. Si ninguno lo hace, el nuevo valor predeterminado no te cuesta nada y puedes actualizar sin leer más.

Ejemplo del mundo real: una carga que se firma

El caso para el que existe el cambio predeterminado. Un documento llega desde el exterior y necesitas añadirle una firma:

with signature.Signature(source_path) as sign:
    options = QrCodeSignOptions("Approved by GroupDocs.Signature")
    options.encode_type = QrCodeTypes.QR
    options.left = 400
    options.top = 50
    options.width = 120
    options.height = 120

    result = sign.sign(output_path, options)

No se solicita ningún recurso externo mientras el documento se carga, se firma o se guarda. La salida firmada conserva su enlace, de modo que un usuario que lo abra más tarde en Word sigue viendo la imagen resuelta en su propia máquina. Omitir la solicitud es una política del lado del servidor, no una edición del documento, que es precisamente lo que lo hace seguro para aplicarlo a archivos que manejas en nombre de otra persona.

¿Qué más cambia al actualizar?

Para la mayoría de los servicios, nada visible, lo cual vale la pena afirmar claramente porque un valor predeterminado de seguridad que altere el comportamiento en todas partes no pasaría una revisión de actualización. Firmado, verificación y búsqueda permanecen intactos. La excepción es una vista previa que antes mostraba una imagen enlazada y ahora muestra un marcador de posición: el cambio está cumpliendo su función. Añade el host a la lista blanca si es tuyo, acéptalo si no lo es.

Merece destacarse por separado: SVG. Un SVG puede referenciar imágenes y hojas de estilo mediante URL; esas referencias son recursos externos bajo la misma regla, y SVG es tanto un formato de carga frecuente como un vector común de SSRF. Un servicio que acepte avatares SVG y los renderice del lado del servidor es precisamente el tipo de sistema que este cambio protege.

Un detalle de Python: cómo se escribe la vista previa

PreviewOptions recibe dos fábricas de streams en lugar de una ruta, y los callables de Python simples son todo lo que necesita:

def create_page_stream(page_data):
    return open(preview_path, "wb")

def release_page_stream(page_data, page_stream):
    page_stream.close()

preview_options = PreviewOptions(create_page_stream, release_page_stream)
preview_options.preview_format = PreviewOptions.PreviewFormats.PNG
sign.generate_preview(preview_options)

Una crea un stream por página, la otra lo libera. El documento de ejemplo tiene una sola página, por lo que se escribe un archivo; para entradas multipágina, incluye el número de página en el nombre o cada página sobrescribirá la anterior.

Conclusión

El valor predeterminado cambió de modo que el comportamiento riesgoso requiere una decisión explícita y el seguro no necesita nada. Mantén el valor predeterminado para entradas no confiables, usa listas blancas restrictivas donde intervengan tus propios hosts y recuerda que el firmado nunca necesitó la red.

Si deseas una verificación más estricta que el tamaño del archivo, apunta un documento de prueba a un host que controles y observa su registro de accesos mientras se genera la vista previa. El tamaño te dice si llegaron bytes; el registro de accesos te dice si se realizó alguna solicitud, y esas dos cosas difieren exactamente en el caso que importa: un host en lista blanca que resulta inaccesible se ve idéntico a uno bloqueado solo con la salida del archivo.

Ejecutar el ejemplo contra uno de tus propios documentos lleva un minuto y te muestra, en tres tamaños de archivo, exactamente lo que tu servicio ha estado solicitando en nombre de quien te envió el archivo.

Recursos adicionales