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