💡 Полный рабочий пример доступен на GitHub:
merge-untrusted-html-uploads-dotnet

Безопасное объединение недоверенных HTML‑документов — это рабочий процесс GroupDocs.Merger для .NET, который отклоняет внешние ресурсы, на которые ссылается загрузка — удалённые URL, UNC‑пути и локальные файлы — если вы явно не разрешите их.

Старый способ был болезненным

Представьте, что ваш портал выставления счетов объединяет загруженный клиентом счёт с накладной того же заказа в один HTML‑документ для архивирования. Счёт — обычный HTML: ссылка на таблицу стилей, изображение логотипа, несколько абзацев — но вы его не писали, его создал браузер клиента или инструмент экспорта. До GroupDocs.Merger 26.10 процесс объединения загружал внешние ресурсы, на которые ссылался HTML — удалённые таблицы стилей, удалённые изображения, абсолютные локальные пути — и не имел возможности остановить это.

Это становится проблемой, когда загрузка не полностью доверена. URL таблицы стилей может быть трекинговым пикселем. Атрибут src изображения может указывать на http://169.254.169.254/latest/meta-data/iam/security-credentials/ — адрес метаданных облака — и ваш шаг объединения запросит его от имени загрузчика. Также читался абсолютный путь file:///, так что ссылка на загрузку другого клиента могла подтянуть этот файл в объединённый результат.

Настоящая цена слепого объединения: задача объединения, которая запрашивает всё, что запрашивает загрузка, превращает ваш сервер в прокси для SSRF, а ссылка file:/// — в способ прочитать файл одного клиента другим клиентом.

Есть лучший способ

GroupDocs.Merger для .NET исправляет это по умолчанию. Начиная с версии 26.10 каждый конструктор LoadOptions устанавливает SkipExternalResources = true, а Merger, созданный без параметров загрузки, использует тот же параметр — внешние ресурсы не загружаются, если вы явно не указали иначе.

Требования:

  • .NET (любой из четырёх целевых фреймворков, поддерживаемых GroupDocs.Merger для .NET)
  • GroupDocs.Merger для .NET 26.10 или новее (получить временную лицензию)
  • Два HTML‑документа для объединения, хотя бы один из источника, которому вы полностью не доверяете

Установить одной командой:

dotnet add package GroupDocs.Merger --version 26.10.0

Новый способ: объединение HTML без доверия к нему

Ниже показано, как объединить два HTML‑запроса с безопасными настройками по умолчанию, обработчиком, фиксирующим каждое решение, и — во втором проходе — обработчиком, предоставляющим один доверенный ресурс.

Шаг 1 — Создание Load Options для формата

LoadOptions не имеет конструктора без параметров и запечатан с версии 26.10; в примере формат указывается явно.

// LoadOptions(FileType.HTML) already denies everything external by default
var audit = new ResourceAudit();
var loadOptions = new LoadOptions(FileType.HTML) { ExternalResourceHandler = audit };

Шаг 2 — Объединение загрузок и проверка решений

// Both uploads follow the options of the Merger they are joined to
using (var merger = new Merger(firstUpload, loadOptions))
{
    merger.Join(secondUpload);
    merger.Save(outputPath);
}

foreach (string decision in audit.Decisions)
{
    Console.WriteLine(decision);
}

Ключевые моменты:

  • SkipExternalResources: здесь неявно true, поэтому каждый удалённый URL, UNC‑путь и ссылка на локальный файл отклоняются, включая файл в собственной папке загрузки.
  • Один LoadOptions на объединение: для второй загрузки отдельные параметры не задаются — она наследует те, с которыми был создан Merger.

Шаг 3 — Разрешить ровно один доверенный ресурс

// Supply the logo's bytes directly instead of requesting it over the network
var trustedAssets = new Dictionary<string, byte[]>
{
    [logoUrl] = File.ReadAllBytes(logoPath)
};
var audit = new ResourceAudit(trustedAssets);
var loadOptions = new LoadOptions(FileType.HTML) { ExternalResourceHandler = audit };

Метод обработчика OnLoading устанавливает args.Skipped = false и args.Data в эти байты, когда запрашиваемый URI совпадает, поэтому документ получает логотип без какого‑либо сетевого запроса со стороны GroupDocs.Merger.

Когда мы запустили объединение по умолчанию для загрузок счёта и накладной, все семь внешних ссылок в обоих файлах были заблокированы — удалённая таблица стилей, удалённый логотип, URL в стиле метаданных, изображение из соседней папки, путь file:///, UNC‑путь и трекинговый URL с параметрами. joined.html сохраняет текст обеих загрузок; вместо каждого заблокированного изображения вставляется заполнитель. При подключённом обработчике доверенного логотипа логотип был «предоставлен», а остальные шесть оставались «заблокированы».

Сравнение: до и после

До (без параметров) После (GroupDocs.Merger 26.10)
Внешние запросы Каждый ресурс загружался автоматически Отклоняется, если явно не разрешено
Абсолютные локальные пути Загружались в результат Отклоняются как любой другой ресурс
Доверенные исключения Не нужны — всё загружалось Один ресурс через обработчик или белый список
Ошибочный элемент белого списка Белого списка не было Выбрасывается ArgumentException при создании
Требуемый код Невозможно ограничить загрузку Пара строк в LoadOptions

Контраст проявляется в том, что возможно, а не только в удобстве: до 26.10 не было настройки, останавливающей загрузку; после — отклонение включено по умолчанию, а каждое исключение должно быть явно указано.

Нарушит ли это HTML, который уже полагался на загрузку удалённых изображений?

Если такой HTML всё ещё объединяется с LoadOptions по умолчанию, да — изображения не загрузятся, и вместо них в выводе появится заполнитель. Исправление должно быть явным, а не обходным: перечислите доверенные источники в WhitelistedResources или подключите IExternalResourceHandler, который предоставит конкретные ресурсы, которые вы всё ещё хотите отобразить. В любом случае процесс объединения продолжится; просто перестанут запрашиваться ресурсы, которые вы не указали.

Пример из реального мира: консолидация счёта и накладной

Конвейер, который объединяет HTML, предоставленный клиентом, в архивную копию, полностью соответствует сценарию, показанному в примере проекта: загрузка счёта и накладной одного заказа, объединённые в один файл. Ни одна из загрузок не была написана командой конвейера, поэтому нельзя считать их безопасными для буквального чтения. Счёт содержит сумму, имя клиента — Northwind Traders, а также ссылки на логотип, трекинговую таблицу стилей, URL в стиле облачных метаданных, изображение из той же папки и путь file:/// — всего семь внешних ссылок между двумя загрузками, ни одна из которых не должна запрашиваться конвейером от имени клиента.

Перед тем как вывести задачу объединения в продакшн, имеет смысл решить для каждого типа документа, нужны ли какие‑либо исключения. В нашем примере сценарий выставления счётов требовал ровно одно исключение: собственный логотип компании, который берётся из файла на диске, а не запрашивается по удалённому URL, указанному в HTML. Всё остальное — трекинговая таблица стилей, адрес в стиле метаданных, соседний файл, UNC‑и file:///‑пути — не имело законных оснований для загрузки, поэтому настройки по умолчанию были правильным решением.

// Reject a whitelist entry that is not an absolute URI prefix
var loadOptions = new LoadOptions(FileType.HTML)
{
    WhitelistedResources = new List<string> { "cdn.example.com" }
};

Передача простого имени хоста вместо абсолютного префикса вроде https://cdn.example.com/brand/ приводит к исключению с сообщением Whitelisted resource 'cdn.example.com' must be an absolute URI without a query, fragment or encoded '/', '\' or '.'. (Parameter ‘WhitelistedResources’) — ещё до открытия любой загрузки. Выявление неверного белого списка при старте приложения ценнее, чем обнаружение проблемы после того, как ресурс молча не загрузился в продакшн.

Что ещё можно сделать с GroupDocs.Merger для .NET?

  • Раннее проверять записи белого списка: некорректный элемент WhitelistedResources приводит к ошибке при создании Merger, поэтому проверки при запуске приложения ловят её до обработки любого документа.
  • Предоставлять доверенные ресурсы с диска: IExternalResourceHandler может вернуть байты, которые уже есть у вас, без какого‑либо сетевого доступа.
  • Применять одну политику ко всем объединяемым документам: LoadOptions задаются один раз для Merger, и каждый последующий документ наследует те же правила.
  • Поддержка не только HTML: текстовые процессоры, HTML/MHTML, презентации, таблицы, SVG и EPUB‑документы также учитывают те же параметры.

Заключение

Объединять HTML, который вы не писали, больше не значит доверять всему, что в нём находится. GroupDocs.Merger для .NET отклоняет внешние ресурсы по умолчанию, начиная с версии 26.10, а два способа предоставить исключения — обработчик доверенных ресурсов и WhitelistedResources — дают ровно столько доступа, сколько необходимо вашему конвейеру, и ничего больше.

Готовы попробовать сами?

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