💡 Полный рабочий пример доступен на 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 от пользователей, настройка по умолчанию — это то, что вам нужно, а белый список — для случаев, когда ваши собственные шаблоны тянут общую таблицу стилей с вашего хоста.

Заключение

По умолчанию теперь требуется явное решение для рискованного поведения, а безопасное — не требует никаких действий. Оставляйте режим по умолчанию для недоверенного ввода, используйте узконаправленный белый список там, где задействованы ваши хосты, и помните, что сама подпись никогда не нуждалась в сети. Запуск примера против одного из ваших документов займет минуту и покажет в трёх размерах файлов, что именно ваш сервис запрашивал.

Дополнительные ресурсы