在C#开发中,XML作为一种通用的数据交换格式被广泛使用,但如果不加防范地解析不可信来源的XML内容,很容易遭遇XML外部实体注入,也就是XXE攻击。攻击者通过在XML中构造恶意的DOCTYPE和实体引用,可以让服务端读取本地文件、探测内网端口甚至执行远程请求。理解解析器的默认行为并主动收紧配置,是避免此类安全问题的核心。

XXE攻击的基本原理
XXE漏洞的根源在于XML规范允许文档定义外部实体,而部分解析器默认会去解析并加载这些实体内容。一个典型的恶意XML示例如下,它定义了一个读取系统文件的外部实体,随后在文档体中输出该实体,服务端若直接解析就会将文件内容返回给攻击者。
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///C:/Windows/win.ini"> ]> <foo>&xxe;</foo>
除了读取本地文件,攻击者还可以利用外部实体发起SSRF,例如将SYSTEM地址指向内网服务地址,从而探测或攻击原本不对外暴露的系统。如果解析器还支持参数实体,还能通过嵌套实现更复杂的盲打攻击。因此,只要应用需要解析外部传入的XML,就必须假定内容不可信。
很多开发者习惯使用XmlDocument.LoadXml方法快速解析字符串,但该方法在旧版本.NET中默认开启DTD处理,等于把实体加载的权限完全放开。只有明确理解每一个解析组件的安全默认值,才能针对性地加固。
使用XmlReader安全解析
在C#中,XmlReader是最底层也最灵活的XML读取器。通过配置XmlReaderSettings,可以精确控制是否允许DTD、是否解析实体。防止XXE的关键设置是将DtdProcessing设为Prohibit,或者设为Ignore,并关闭XmlResolver。
using System;
using System.Xml;
class SafeXmlDemo
{
static void Main()
{
string xml = "<?xml version="1.0"?><root>hello</root>";
XmlReaderSettings settings = new XmlReaderSettings();
// 禁止处理DTD,从根本上阻断外部实体
settings.DtdProcessing = DtdProcessing.Prohibit;
settings.XmlResolver = null;
using (XmlReader reader = XmlReader.Create(new System.IO.StringReader(xml), settings))
{
while (reader.Read())
{
Console.WriteLine(reader.NodeType + ": " + reader.Name);
}
}
}
}
上面的代码将DtdProcessing设为Prohibit后,如果传入的XML包含DOCTYPE声明,解析器会直接抛出异常,而不是尝试加载实体。同时把XmlResolver设为null,即使某些配置下允许DTD,也不会有任何解析器去获取外部资源。
在.NET Framework较新版本和.NET Core、.NET 5+中,XmlReader的默认设置已经趋于安全,但显式声明配置能避免环境差异带来的隐患。建议在所有解析入口统一封装一个创建安全XmlReader的方法,供团队复用。
XmlDocument与XDocument的加固
如果业务代码已经使用了XmlDocument或XDocument,同样可以通过传入安全配置来防护。XmlDocument可以结合XmlReader来加载内容,而不是用LoadXml直接处理字符串。
using System;
using System.Xml;
class XmlDocSafe
{
static void ParseWithXmlDocument(string xmlContent)
{
XmlReaderSettings settings = new XmlReaderSettings();
settings.DtdProcessing = DtdProcessing.Prohibit;
settings.XmlResolver = null;
XmlDocument doc = new XmlDocument();
using (XmlReader reader = XmlReader.Create(new System.IO.StringReader(xmlContent), settings))
{
doc.Load(reader);
}
Console.WriteLine(doc.OuterXml);
}
}
对于LINQ to XML中的XDocument,在.NET中可以通过设置XmlReader来间接控制安全性,因为XDocument.Load也接受XmlReader实例。这样既能继续使用XPath或LINQ查询的便利,又不会牺牲安全性。
需要注意的是,有些第三方库或旧代码可能内部直接调用了不安全的解析接口。在代码审查时应全局搜索LoadXml、XmlTextReader构造等用法,统一替换为受控的XmlReader模式,避免遗漏单点造成整体防护失效。
常见误区与检查清单
一个常见误区是认为只要不输出实体内容就不会有风险。实际上,即使应用不回显解析结果,攻击者仍可能利用盲XXE通过外部服务器接收带外数据,或者借助错误响应来推断文件内容。因此关闭实体解析是唯一稳妥的做法。
另一个误区是依赖输入校验来拦截恶意XML。由于XML结构复杂,单纯的关键字过滤很容易被编码或嵌套绕过。正确的做法是限制解析器的能力,而不是试图在内容层面做黑名单。
| 解析方式 | 默认是否危险 | 安全做法 |
|---|---|---|
| XmlReader.Create | 新版较安全 | 显式DtdProcessing=Prohibit |
| XmlDocument.LoadXml | 旧版危险 | 改用XmlReader加载 |
| XmlTextReader | 危险 | 避免使用或严格配置 |
建议团队在开发规范中写明:所有解析外部XML的路径必须使用统一的安全配置,并在单元测试中加入包含DOCTYPE的样例,验证解析器确实拒绝处理。这样能把XXE防护固化到研发流程里。
总结
防止C#中的XXE攻击并不复杂,核心就是不让解析器处理不可信的DTD和外部实体。通过XmlReaderSettings关闭DtdProcessing、置空XmlResolver,再把这些配置应用到XmlDocument和XDocument的加载过程,就能有效切断攻击链路。把安全解析封装为公共方法,比零散地修补更容易长期维护。