什么是XML炸弹(Billion Laughs Attack)

XML炸弹本质是利用XML解析器对实体的递归展开机制,以几百字节输入触发GB级内存消耗;Python xml.etree.ElementTree默认未禁用DTD和外部实体,易受Billion Laughs等攻击;防御需主动禁用DOCTYPE、使用defusedxml等安全替代库,并杜绝日志回显原始XML。

XML炸弹本质:用几百字节触发GB级内存爆炸

XML炸弹不是传统意义上的“恶意代码”,而是一种精心构造的、语法完全合法的XML文档,它利用XML解析器对实体(entity)的递归展开机制,在极小输入下引发指数级资源消耗。一个典型例子展开后能吃掉3GB内存——攻击者只发几百字节,服务器就可能瞬间卡死或崩溃。

为什么xml.etree.ElementTree默认会中招

Python标准库的xml.etree.ElementTreefromstring()parse()时,若未显式禁用DTD和外部实体,就会处理中定义的嵌套实体。而Billion Laughs正是靠这种“宏展开”实现指数增长:


  
  
]>
&c;

这里&c;展开为5×5=25个lol,再深一层就是125个——5层嵌套就能生成超过3亿个字符串副本。

  • 默认行为:ElementTree不校验也不限制实体展开深度或总展开长度
  • 风险场景:处理用户上传的XML、接收SOAP请求、解析第三方配置文件
  • 关键误区:以为“没用lxml就安全”——标准库同样脆弱

Java/PHP/NET里怎么防:三步硬性配置

所有主流语言的XML解析器都提供禁用机制,但必须主动开启,不能依赖默认值:

  • Java(SAX/DOM):设置setFeature("http://apache.org/xml/features/disallow-doctype-decl", true),或使用DocumentBuilderFactory.setFeature("http://javax.xml.XMLConstants/feature/secure-processing", true)
  • PHP(libxml):调用libxml_disable_entity_loader(true)(注意:该函数在PHP 8.0+已废弃,需改用LIBXML_NOENT | LIBXML_DTDATTR组合并手动过滤)
  • .NET(System.Xml):将XmlReaderSettings.DtdProcessing设为DtdProcessing.Prohibit,且XmlResolver = null

漏掉任意一项,XXE或XML炸弹都可能绕过防护。

真正有效的防御不是“堵”,而是“换”

禁用DTD只是底线,不是终点。生产环境应优先切换到更安全的替代方案:

  • defusedxml(Python)替代原生xml.etree
    from defusedxml.ElementTree import parse
    tree = parse(file_obj)  # 自动禁用所有危险特性
  • xmlreader(PHP)替代simplexml_load_string,配合XMLReader::open() + setParserProperty(XMLReader::SUBST_ENTITIES, false)
  • 拒绝解析含/code>的XML——很多业务根本不需要DTD,直接在入口层丢弃即可

最常被忽略的一点:日志、监控、调试接口如果会把原始XML回显或存入日志,哪怕你解析器本身是安全的,攻击者也能通过报错信息反推服务器路径或触发盲注外带——XML安全从来不只是解析器的事。