💡 Повний робочий приклад доступний на GitHub:
load-untrusted-documents-safely-python

Старий спосіб був болючим

Ви написали три рядки, щоб відобразити мініатюру завантаженого документа. Вони виглядали так і працювали добре:

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

Що ці рядки робили до випуску GroupDocs.Signature 26.9, так це отримували кожну адресу, на яку вказував документ. Файл Word може містити зображення, яке він не містить — файл зберігає URL, і будь‑яка програма, що його відкриває, завантажує цей URL. На настільному комп’ютері це функція. На сервері, що приймає завантаження, це означає, що відправник файлу вирішує, які адреси запитуватиме ваша інфраструктура.

Атака має назву server‑side request forgery і має три варіанти, які варто назвати. Внутрішня адреса, недоступна з інтернету, доступна з вашого сервера, тому підготовлений документ може змусити ваш сервіс запросити http://169.254.169.254/ або адміністративну точку входу на localhost. UNC‑шлях може спонукати Windows‑хост аутентифікуватися назовні, передаючи облікові дані серверу, контрольованому атакувальником. А посилання на хост, який просто не відповідає, утримує потік завантаження до тайм‑ауту, що є дешевим способом вичерпати пул робітників документами, які виглядають безпечними.

Ніщо з цього не є помилкою в бібліотеці документів. Перехід за посиланням — це те, що вимагає формат. Дискомфортною частиною було те, що таке виконання було за замовчуванням, і в коді ніхто не позначав це під час рев’ю.

Є кращий спосіб

Безпечне завантаження документів — це поведінка GroupDocs.Signature для Python, яка відмовляється робити такі запити. Починаючи з версії 26.9, LoadOptions.skip_external_resources за замовчуванням встановлюється в True, тому ті ж три рядки тепер нічого не запитують і відображають заповнювач там, де мала бути пов’язана картинка.

Зміна — це зміна за замовчуванням, а не нова функція — властивість вже існувала. Те, що змінив 26.9, це куди вона вказує, коли ваш код нічого не зазначає, і це єдиний параметр, який використовують більшість сервісів.

Новий спосіб: три режими завантаження

Крок 1 — Залиште значення за замовчуванням для будь‑якого недовіреного вмісту

Без LoadOptions взагалі:

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

Нічого не запитується. Попередній перегляд менший, ніж був би інакше, і ця різниця в розмірі — найзручніше доказ, що жоден запит не залишив машину.

Крок 2 — Додайте у білий список хост, яким ви дійсно володієте

Багато документів посилаються кудись легітимно: на CDN компанії, внутрішній сервер зображень, сховище шаблонів. Дозвольте це і нічого більше:

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)

Правило відповідності потребує уваги. Це нечутливий до регістру підрядковий тест адреси ресурсу, що робить короткий фрагмент небезпечним: github збігається з github.attacker.example/payload.png так само легко, як і з потрібним вам хостом. Використовуйте схему, хост і шлях — у цьому прикладі у білий список додається raw.githubusercontent.com/groupdocs-signature/.

Крок 3 — Дозволити все навмисно

Поведінка до 26.9, все ще доступна:

load_options = LoadOptions()
load_options.skip_external_resources = False

Розумно для документів, створених вашою власною програмою. Одна пастка: застаріла властивість load_external_resources має протилежну полярність, тому skip_external_resources = False замінює load_external_resources = True. Скопіювавши значення зі старої властивості, ви інвертуєте свою безпеку без жодного повідомлення про помилку.

Порівняння: до і після

Той самий документ, той самий шлях коду, три політики завантаження. Це розміри файлів, закомічених у папці Result/ прикладу, їх можна перевірити, а не приймати на віру:

Режим завантаження Розмір попереднього перегляду Вихідні запити
за замовчуванням (26.9 і пізніше) 16 435 байт нічого
хост у білому списку 51 738 байт один, до дозволеної адреси
всі ресурси (за замовчуванням до 26.9) 51 738 байт по одному на кожен зв’язаний ресурс

Пов’язане зображення становить 35 303 байт цієї різниці. Я не довіряв налаштуванню, доки ці два числа не були поруч, і рекомендую те ж саме: читання властивості назад показує, що ви налаштували, а не що процес зробив.

Що вважається зовнішнім ресурсом?

Вузько, ніж очікує більшість, саме тому оновлення зазвичай проходить без проблем. Це пов’язані зображення, а не вбудовані, поля INCLUDEPICTURE, пов’язані зображення в презентаціях і електронних таблицях, а також зображення та таблиці стилів, на які посилається SVG. Вбудований вміст не змінюється, бо він вже всередині файлу і не потребує запиту для відображення.

Це розмежування — вся безпекова межа. Документ може змусити ваш сервер виходити назовні лише тоді, коли він зберігає адресу замість байтів, тому питання для будь‑якого корпусу просте: скільки його файлів посилаються, а не вбудовують. Якщо жоден не посилається, нове значення за замовчуванням нічого вам не коштує, і ви можете оновитися без додаткового читання.

Реальний приклад: завантаження, яке підписується

Випадок, для якого існує зміна за замовчуванням. Документ надходить ззовні, і вам потрібно підписати його:

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)

Під час завантаження, підпису чи збереження зовнішній ресурс не запитується. Підписаний вихід зберігає своє посилання, тому користувач, який відкриє його в Word пізніше, все ще побачить зображення, завантажене на його власному комп’ютері. Пропуск — це політика на боці сервера, а не зміна документа, що саме робить її безпечною для файлів, якими ви оперуєте від імені іншої особи.

Що ще змінюється після оновлення?

Для більшості сервісів нічого помітного, і це варто чітко зазначити, бо безпекове значення за замовчуванням, яке змінює поведінку скрізь, не пройде рев’ю оновлення. Підписування, верифікація та пошук залишаються без змін. Виняток — попередній перегляд, який раніше показував пов’язане зображення, а тепер — заповнювач; зміна виконує свою задачу. Додайте хост у білий список, якщо це ваш, прийміть його, якщо ні.

Окремо варто згадати: SVG. SVG може посилатися на зображення та таблиці стилів за URL, ці посилання — зовнішні ресурси за тим же правилом, і SVG одночасно є поширеним форматом завантаження та поширеним вектором SSRF. Сервіс, який приймає SVG‑аватари і рендерить їх на боці сервера, саме той, кого захищає ця зміна.

Одна деталь Python: як записується попередній перегляд

PreviewOptions приймає два фабричні методи потоків замість шляху, і прості виклики Python — це все, що потрібно:

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)

Один створює потік на сторінку, інший його закриває. У прикладі документ має одну сторінку, тому записується один файл; для багатосторінкового вводу включайте номер сторінки у назву або кожна сторінка перезаписує попередню.

Висновок

Значення за замовчуванням змінилося так, що ризикова поведінка вимагає явного рішення, а безпечна — нічого. Залишайте значення за замовчуванням для недовіреного вводу, вузько додавайте у білий список лише ваші хости і пам’ятайте, що підписування ніколи не потребувало мережі.

Якщо потрібна більш сувора перевірка, ніж розмір файлу, вкажіть тестовий документ на хост, яким ви керуєте, і спостерігайте його журнал доступу під час роботи попереднього перегляду. Розмір підкаже, чи прийшли байти; журнал доступу — чи був взагалі запит, і ці два показники різняться саме в тому випадку, який має значення — білий список, який недоступний, виглядає ідентично заблокованому лише за вихідними даними.

Запуск прикладу проти одного зі своїх документів займе хвилину і покаже вам у трьох розмірах файлів точно, що ваш сервіс запитував від імені того, хто надіслав вам файл.

Додаткові ресурси