💡 Полный рабочий пример доступен на 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/или административный эндпоинт на 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. Скопировав значение из старого свойства, вы инвертируете свою позицию по безопасности без какого‑либо предупреждения.
Сравнение трёх режимов: когда использовать каждый
| Режим | Лучшее применение | Ключевые преимущества | Ограничения |
|---|---|---|---|
| По умолчанию (skip) | загрузки от пользователей, электронная почта, файлы партнёров | невозможен ни один исходящий запрос | связанные изображения отображаются как заполнитель |
| Белый список | документы, ссылающиеся на ваш собственный хост | сохраняет работу легитимных ссылок | проверка подстроки требует длинного, однозначного фрагмента |
| Разрешить всё | файлы, сгенерированные вашими системами | предпросмотр выглядит точно так же, как раньше | восстанавливает уязвимость 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 от пользователей, настройка по умолчанию — это то, что вам нужно, а белый список — для случаев, когда ваши собственные шаблоны тянут общую таблицу стилей с вашего хоста.
Заключение
По умолчанию теперь требуется явное решение для рискованного поведения, а безопасное — не требует никаких действий. Оставляйте режим по умолчанию для недоверенного ввода, используйте узконаправленный белый список там, где задействованы ваши хосты, и помните, что сама подпись никогда не нуждалась в сети. Запуск примера против одного из ваших документов займет минуту и покажет в трёх размерах файлов, что именно ваш сервис запрашивал.