测试Scala Play接收含DTD/XXE的XML时的行为差异
嘿,我碰到过类似的坑,Play默认的XXE防护确实会拦截含DTD的XML,但测试和真实请求环境的差异很容易导致行为不一致,下面给你拆解几个常见原因和解决办法:
1. 单元测试用错了XML解析器
Play在真实请求里用的是自带的play.api.libs.xml.XML,这个工具类已经默认禁用了DTD解析。但很多人在测试里图方便用了Scala原生的scala.xml.XML.loadString(),这俩完全不是一回事——原生解析器允许DTD,自然和Postman请求的结果不一样!
测试里得用Play提供的解析工具才对:
import play.api.libs.xml.XML // 测试含DTD的XML应该被拒绝 val unsafeXml = """<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><foo>&xxe;</foo>""" // 这里应该直接抛出解析异常,或者返回错误结果 val attempt = scala.util.Try(XML.loadString(unsafeXml)) assert(attempt.isFailure, "含DTD的XML应该被Play的安全解析器拒绝")
2. 测试没走完整的Play路由流程
如果你的单元测试是直接调用控制器方法,而没通过Play的FakeRequest模拟完整请求链路,那相当于绕开了Play的请求体解析中间件——XXE防护正是在这个中间件里生效的!
正确的测试姿势是用FakeRequest模拟真实请求:
import play.api.test._ import play.api.test.Helpers._ val app = new GuiceApplicationBuilder().build() val badXml = <!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><foo>&xxe;</foo> val request = FakeRequest(POST, "/your-target-endpoint") .withXmlBody(badXml) val result = route(app, request).get // 正常应该返回400 Bad Request或者类似的错误状态码 assert(status(result) == BAD_REQUEST, "含DTD的请求应该被Play拦截")
3. 检查配置文件的差异
如果你在application.conf里修改过XML解析的配置(比如不小心开启了DTD支持),或者测试环境用了不同的配置文件(比如application.test.conf),也会导致行为不一致。
确保配置里的这些项是默认的安全设置:
# 禁用DTD和外部实体,Play默认就是这个配置 play.xml.parser.dtd.disabled = true play.xml.parser.external-general-entities = false play.xml.parser.external-parameter-entities = false
4. 确认Postman发送的内容没被篡改
有时候Postman会自动转义XML里的特殊字符,导致你以为发了含DTD的请求,但Play实际收到的是被转义后的内容(比如<!DOCTYPE变成了<!DOCTYPE),自然不会触发拦截。
可以打开Postman的「Raw」模式,复制粘贴原始XML,同时在Play的日志里打印请求体,确认实际收到的内容和你预期的一致。
补充下James Roper提到的点:Play的默认XXE防护只针对通过内置请求解析器处理的XML生效,只要你没手动修改解析器配置、没绕开请求处理流程,正常情况下含DTD的XML请求都会被直接拒绝。
先从这几个点排查,应该就能解决测试和Postman行为不一致的问题啦!
内容的提问来源于stack exchange,提问作者James

