C#如何安全地解析XML以防止XXE注入

默认XmlDocument和XmlReader触发XXE因.NET默认启用DTD解析,需同时设DtdProcessing.Prohibit和XmlResolver=null;XmlSerializer也需用加固XmlReader;还需限制MaxDepth、MaxCharactersInDocument等防DoS。

为什么默认的 XmlDocumentXmlReader 会触发XXE

因为 .NET 默认启用 DTD 解析(XmlResolver 非空),当 XML 包含 ]> 这类声明时,XmlDocument.Load()XmlReader.Create() 会尝试解析并加载外部实体,导致文件读取、网络请求甚至 SSRF。

禁用 DTD 解析的两种可靠方式

核心是让解析器完全忽略 DOCTYPE 声明,不执行任何外部实体解析。推荐以下任一做法:

  • 使用 XmlReaderSettings 显式关闭:
    var settings = new XmlReaderSettings
    {
        DtdProcessing = DtdProcessing.Prohibit, // 关键:禁止 DTD 处理
        XmlResolver = null                      // 关键:移除解析器
    };
    using var reader = XmlReader.Create(stream, settings);
    var doc = new XmlDocument();
    doc.Load(reader);
  • XmlDocument 直接设置:
    var doc = new XmlDocument();
    doc.XmlResolver = null; // 必须在 Load 前设置
    doc.Load(xmlStream);
    注意:此方式仅在 .NET Framework 4.5.2+ 和 .NET Core 2.0+ 有效;旧版需配合 ProhibitDtd = true(但该属性已过时)

别踩 XmlSerializer 的坑

XmlSerializer 默认不解析 DTD,看似安全,但它会隐式使用 XmlReader,若你传入的是未加固的 XmlReader 实例,风险仍在。安全做法是始终用加固后的 XmlReader 构造:

var settings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null };
using var reader = XmlReader.Create(stream, settings);
var serializer = new XmlSerializer(typeof(MyClass));
var obj = serializer.Deserialize(reader);

不要直接传 streamDeserialize() —— 它会内部创建不设防的 reader。

额外加固:限制解析深度与大小

防止恶意构造的超深嵌套或超大文本耗尽内存:

  • MaxDepth = 10(默认 64,建议压到 8–12)
  • MaxCharactersInDocument = 10 * 1024 * 1024(如限制 10MB)
  • MaxCharactersFromEntities = 10240(防实体膨胀攻击)

这些都应在 XmlReaderSettings 中一并配置,否则单禁 DTD 仍可能被 DoS。

XXE 防御不是“关一个开关”就完事;DtdProcessing.ProhibitXmlResolver = null 必须同时生效,且所有入口(XmlDocumentXmlReaderXmlSerializer)都要走同一套加固逻辑。漏掉任意一种路径,攻击面就还在。