💡 Повний робочий приклад доступний на GitHub:
skip-external-resources-when-signing-dotnet
Вступ
Документ Word може містити зображення, яке не зберігається у файлі. Документ містить адресу, і будь‑яка програма, яка його відкриває, завантажує цю адресу. На настільному комп’ютері це корисна функція — зображення оновлюється разом із джерелом. На сервері, що приймає завантаження, це означає, що відправник файлу вирішує, які URL‑адреси ваш інфраструктурний код запитуватиме.
Безпечне завантаження документів — це поведінка GroupDocs.Signature для .NET, яка відмовляється виконувати такі запити. Починаючи з версії 26.9, LoadOptions.SkipExternalResources за замовчуванням має значення true. У цій статті порівнюються три режими завантаження на одному і тому ж документі, показано, як дозволити один хост без дозволу всіх, і пояснено, чому підписання недовіреного файлу не потребує жодного мережевого доступу.
Чому це важливо більше, ніж здається
Атака має назву — server‑side request forgery (SSRF) — і три конкретні форми.
Внутрішня адреса, недоступна з інтернету, доступна вашому серверу, тому спеціально підготовлений документ може змусити ваш сервіс запитати http://169.254.169.254/ або адміністративний endpoint на localhost і, залежно від того, що ви робите з результатом, витікнути його. UNC‑шлях у документі може спонукати Windows‑хост аутентіфікуватися назовні, передаючи облікові дані серверу, контрольованому атакувальником. А посилання на хост, який просто не відповідає, блокує потік завантаження до тайм‑ауту, що є дешевим способом вичерпати пул робітників.
Я вважав це теоретичною проблемою, доки не спостерігав, як тестовий документ завантажує зображення через сервіс, який взагалі не повинен робити вихідні запити. Для цього не потрібен баг у бібліотеці документів. Перехід за посиланням — це те, що вимагає формат; питання лише в тому, чи ваш сервер повинен це виконувати.
Метод 1 — нове значення за замовчуванням
Без LoadOptions взагалі:
using var signature = new Signature(sourcePath);
return SavePagePreview(signature, previewPath);
Нічого не завантажується. Попередній перегляд показує порожнє місце, де повинно було бути пов’язане зображення, а PNG‑файл менший, ніж був би інакше. Ця різниця в розмірі — найзручніше підтвердження того, що жоден запит не залишив машину.
Які функції вважаються зовнішніми? Пов’язані зображення замість вбудованих, поля INCLUDEPICTURE, пов’язані зображення в презентаціях і електронних таблицях, а також зображення та таблиці стилів, на які посилається SVG. Вбудований вміст не змінюється — він вже знаходиться у файлі.
Метод 2 — білий список одного адреси
Багато документів посилаються на легітимні ресурси: CDN компанії, внутрішній сервер зображень, сховище шаблонів. Дозвольте лише його і нічого більше:
var loadOptions = new LoadOptions
{
WhitelistedResources = new List<string> { trustedAddress }
};
using var signature = new Signature(sourcePath, loadOptions);
Правило збігу заслуговує уваги. Це нечутливий до регістру тест підрядка проти адреси ресурсу, що означає, що короткий фрагмент небезпечний: github збігається з github.attacker.example/payload.png так само легко, як і з потрібним хостом. Використовуйте схему, хост і шлях — у прикладі у білий список включено raw.githubusercontent.com/groupdocs-signature/.
Метод 3 — дозволити все
Поведінка до 26.9, все ще доступна:
var loadOptions = new LoadOptions { SkipExternalResources = false };
Розумно для документів, створених вашою власною програмою. Одна пастка, яку варто зазначити: застаріла властивість LoadExternalResources має протилежну полярність, тому SkipExternalResources = false замінює LoadExternalResources = true. Скопіювавши значення зі старої властивості, ви інвертуєте свою безпеку без жодного повідомлення про помилку.
Порівняння трьох режимів: коли який використовувати
| Режим | Кращий для | Ключові переваги | Обмеження |
|---|---|---|---|
| За замовчуванням (пропуск) | завантаження користувачами, електронна пошта, файли партнерів | неможливі вихідні запити | пов’язані зображення відображаються як заповнювачі |
| Білий список | документи, що посилаються на ваш власний хост | зберігає роботу легітимних посилань | збіг підрядка вимагає довгого, конкретного фрагмента |
| Дозволити все | файли, створені вашими системами | попередній перегляд виглядає точно так само, як раніше | відновлює SSRF‑вразливість, яку усунуло значення за замовчуванням |
Чи потрібні ресурси під час підписання?
Ні, і це практичний виграш. Підпис QR‑коду застосовується з налаштуваннями за замовчуванням, і жоден зовнішній ресурс не запитується під час завантаження, підписання чи збереження документа:
var options = new QrCodeSignOptions("Approved by GroupDocs.Signature")
{
EncodeType = QrCodeTypes.QR,
Left = 400,
Top = 50,
Width = 120,
Height = 120
};
SignResult result = signature.Sign(outputPath, options);
Підписаний вихід зберігає своє посилання, тому користувач, який відкриє документ пізніше, все ще побачить зображення, завантажене на його машині. Пропуск — це політика на боці сервера, а не зміна документа, що робить її безпечною для застосування до файлів, які ви обробляєте від імені інших.
Що змінюється при оновленні
Для більшості сервісів нічого помітного спочатку, і це варто чітко зазначити, бо безпекове значення за замовчуванням, яке змінює поведінку скрізь, не пройде ревізію оновлення. Виняток — будь‑де, де попередній перегляд або мініатюра раніше показували пов’язане зображення, а тепер — заповнювач; це саме зміна, яка виконує свою роботу, і виправлення — додати запис у білий список, якщо хост ваш, або прийняти, якщо документ прийшов зовні.
Чесний спосіб перевірки — той, що використано у прикладі: відрендерити один і той же документ у всіх трьох режимах і порівняти розміри вихідних файлів. Якщо попередні перегляди за замовчуванням і з білим списком однакові за розміром, нічого не було завантажено в обох випадках — зазвичай це означає, що хост недоступний з даної машини, а не що білий список не спрацював, і приклад виводить підказку саме про це.
Помічник попереднього перегляду, бо це не очевидно
Два з трьох режимів вище викликають невеликий помічник, і його варто показати, бо PreviewOptions не приймає шлях:
var previewOptions = new PreviewOptions(
pageData => File.Create(previewPath),
(pageData, pageStream) => pageStream.Dispose())
{
PreviewFormat = PreviewOptions.PreviewFormats.PNG
};
signature.GeneratePreview(previewOptions);
Він приймає два фабричні методи потоків — один для створення потоку на кожну сторінку, інший для його звільнення. У прикладі документ має одну сторінку, тому записується один файл; для багатосторінкового вводу включайте номер сторінки у назву файлу, інакше кожна сторінка перезаписуватиме попередню.
Найкращі практики
- Ставте все, що ви не створили, під недовірливий статус, включаючи файли від партнерів з високим рівнем безпеки.
- Робіть фрагменти білого списку достатньо довгими, щоб бути однозначними, і переглядайте їх, коли CDN змінює свої адреси.
- Ніколи не встановлюйте
SkipExternalResourcesзі значення, яке раніше присвоювалосяLoadExternalResources. - Перевіряйте за розмірами вихідних файлів, а не лише за налаштуванням; конфігурація, що виглядає правильно, і відсутність запиту — це різні твердження.
Де це залишає SVG
Варто окремо зазначити, бо SVG — це одночасно поширений формат завантаження і поширений вектор SSRF. SVG може посилатися на зображення та таблиці стилів за URL, і ці посилання є зовнішніми ресурсами за тим же правилом — за замовчуванням пропускаються, їх можна додати до білого списку, їх можна відновити. Сервіс, який приймає SVG‑аватари або логотипи і рендерить їх на боці сервера, саме той, який захищає ця зміна.
Якщо ваш конвеєр приймає SVG від користувачів, значення за замовчуванням — це те, що вам потрібно, а білий список — для випадків, коли ваші власні шаблони підтягують спільну таблицю стилів з вашого хоста.
Висновок
За замовчуванням тепер ризикована поведінка вимагає явного рішення, а безпечна — нічого. Тримайте значення за замовчуванням для недовірених вхідних даних, вузько обмежуйте білий список там, де залучені ваші хости, і пам’ятайте, що саме підписання ніколи не потребувало мережі. Запуск прикладу проти одного зі своїх документів займе хвилину і покаже вам у трьох розмірах файлів, що саме ваш сервіс запитував.