💡 完整的工作示例可在 GitHub 上获取: merge-untrusted-html-uploads-dotnet

安全地合并不受信任的 HTML 文档是一个 GroupDocs.Merger 工作流,默认会拒绝上传中引用的外部资源——远程 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 for .NET 在默认设置上解决了这个问题。自 26.10 版本起,所有 LoadOptions 构造函数都会将 SkipExternalResources = true,而未指定加载选项创建的 Merger 也使用同样的默认值——除非你另行声明,否则不会加载任何外部资源。

先决条件:

  • .NET(GroupDocs.Merger for .NET 支持的四个目标框架中的任意一个)
  • GroupDocs.Merger for .NET 26.10 或更高版本(获取临时许可证)
  • 两个待合并的 HTML 文档,至少其中一个来源不完全可信

使用一条命令安装:

dotnet add package GroupDocs.Merger --version 26.10.0

新方式:在不信任的情况下合并 HTML

下面展示了在安全默认设置下合并两个 HTML 上传的完整流程,包括记录每个决策的处理器,以及在第二轮中提供一个受信任资产的处理器。

步骤 1 - 为格式创建 Load Options

LoadOptions 自 26.10 起没有无参构造函数且已被密封;示例中显式指定了格式。

// LoadOptions(FileType.HTML) 已默认拒绝所有外部资源
var audit = new ResourceAudit();
var loadOptions = new LoadOptions(FileType.HTML) { ExternalResourceHandler = audit };

步骤 2 - 合并上传并检查决策

// 两个上传都遵循它们所加入的 Merger 的选项
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 - 仅允许一个受信任的资源

// 直接提供徽标的字节,而不是通过网络请求获取
var trustedAssets = new Dictionary<string, byte[]>
{
    [logoUrl] = File.ReadAllBytes(logoPath)
};
var audit = new ResourceAudit(trustedAssets);
var loadOptions = new LoadOptions(FileType.HTML) { ExternalResourceHandler = audit };

处理器的 OnLoading 方法在请求的 URI 与提供的键匹配时将 args.Skipped = false 并把 args.Data 设为这些字节,从而在文档中获得徽标,而 GroupDocs.Merger 不会发起任何网络请求。

当我们使用默认合并对发票和送货单上传进行测试时,两个文件中共七个外部引用全部被阻止——远程样式表、远程徽标、元数据式 URL、同文件夹图片、file:/// 路径、UNC 路径以及带查询字符串的跟踪 URL。joined.html 保留了两个上传的文本内容;每个被阻止的图片位置都有占位符。挂载受信任徽标处理器后,徽标被 supplied(提供),其余六个仍保持 blocked(阻止)。

对比:之前 vs. 之后

之前(无选项) 之后(GroupDocs.Merger 26.10)
外部请求 每个引用都会自动加载 除非显式允许,否则全部拒绝
绝对本地文件路径 会被加载到输出中 与其他引用一样被拒绝
受信任例外 不需要——所有内容都会加载 通过处理器或白名单前缀仅允许单个资源
错误的白名单条目 没有白名单 在构造时抛出 ArgumentException
所需代码 没有办法限制加载 只需在 LoadOptions 上写几行代码

对比的重点在于“可能性”,而不仅仅是“便利性”:在 26.10 之前没有任何设置可以阻止加载;在之后,默认即为拒绝,所有例外必须显式声明。

这会破坏已经依赖远程图片加载的 HTML 吗?

如果该 HTML 仍使用默认 LoadOptions 合并,答案是 会——图片将不再加载,输出中会出现占位符。解决方案是显式的,而非变通:在 WhitelistedResources 中列出受信任的来源,或挂载 IExternalResourceHandler 来提供你仍希望渲染的特定资产。无论哪种方式,合并仍会继续,只是停止获取未被命名的任何资源。

真实案例:发票与送货单合并

一个将客户提交的 HTML 合并进归档副本的文档流水线正是本页面示例项目所建模的场景:一份发票上传和同一订单的送货单被合并为一个文件。两份上传都不是流水线团队编写的,因此不能假设它们可以安全地原样读取。发票中包含应付金额、客户名称(Northwind Traders),以及徽标引用、跟踪样式表、云元数据式 URL、同文件夹图片引用和 file:/// 路径——共七个外部引用,归档流水线不应代表客户去获取这些资源。

在合并作业上线前,最好逐类文档决定是否真的需要例外。本示例的开票场景只需要一个例外:公司自己的徽标,从磁盘上的文件直接提供,而不是从 HTML 中声明的远程 URL 获取。其余所有资源——跟踪样式表、元数据式地址、同目录文件、UNC 和 file:/// 路径——都没有合理的获取理由,因此默认设置已经是正确的答案。

// 拒绝不是绝对 URI 前缀的白名单条目
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 for .NET 还能做什么?

  • 提前验证白名单条目:格式错误的 WhitelistedResources 条目会在 Merger 构造时失败,部署启动检查即可在任何文档处理之前捕获。
  • 从磁盘提供受信任资产:IExternalResourceHandler 可以直接返回已有的字节数据,完全不需要网络访问。
  • 对每个加入的文档统一策略:LoadOptions 只需在 Merger 上设置一次,随后加入的所有文档都会遵循相同规则。
  • 覆盖除 HTML 之外的更多格式:文字处理、HTML/MHTML、演示文稿、电子表格、SVG 与 EPUB 文档同样遵守这些选项。

结论

合并非你编写的 HTML 不再意味着要信任其中的所有内容。自 26.10 版本起,GroupDocs.Merger for .NET 默认拒绝外部资源,而通过受信任资产处理器或 WhitelistedResources 两种方式提供例外,能够恰到好处地满足流水线的实际需求,既不多也不少。

准备好自己动手试试了吗?

其他资源