用 defusedxml 安全解析用户上传 XML:禁用 DTD 并返回 400

举报
yd_240403232 发表于 2026/11/12 13:56:39 2026/11/12
【摘要】 用户上传XML若直接用lxml解析,可能触发XXE与实体扩展风险。本文给出用defusedxml禁用DTD和外部实体、捕获DTDForbidden与EntitiesForbidden并返回400的最小Flask实现,附运行方法与XXE验证payload及边界说明。

问题场景

用户上传 XML 的接口常见写法是直接用 lxml 的 etree.fromstring 解析。lxml 默认允许 DTD 和外部实体,攻击者可以构造带 SYSTEM 实体的 XML,尝试读取服务器本地文件或发起外部请求;实体扩展攻击也可能消耗解析资源。对外接口应默认拒绝 DTD 和实体,而不是依赖解析器的宽松默认值。

方案思路

defusedxml 在标准 XML 解析器外包了一层安全策略。替换 lxml 时,关键点是显式传入以下参数:

  • forbid_dtd=True:遇到 DOCTYPE 立即抛出 DTDForbidden。
  • forbid_entities=True:禁止实体定义时抛出 EntitiesForbidden。
  • forbid_external=True:禁止外部实体。

同时,在应用层限制上传字节数,避免超大文件进入解析器。异常统一返回 400,不把底层异常文本、文件路径或解析栈返回给调用方。

最小 Flask 代码

from flask import Flask, request, jsonify
from defusedxml import ElementTree as DET
from defusedxml.common import DTDForbidden, EntitiesForbidden

app = Flask(__name__)
MAX_XML_BYTES = 256 * 1024

@app.post('/upload-xml')
def upload_xml():
    file = request.files.get('file')
    if file is None:
        return jsonify(error='missing file'), 400

    raw = file.read(MAX_XML_BYTES + 1)
    if len(raw) > MAX_XML_BYTES:
        return jsonify(error='file too large'), 413

    try:
        root = DET.fromstring(
            raw,
            forbid_dtd=True,
            forbid_entities=True,
            forbid_external=True,
        )
    except DTDForbidden:
        return jsonify(error='DTD forbidden'), 400
    except EntitiesForbidden:
        return jsonify(error='entities forbidden'), 400
    except Exception:
        return jsonify(error='invalid xml'), 400

    return jsonify(root_tag=root.tag)

if __name__ == '__main__':
    app.run(debug=False)

说明:先捕获 DTDForbidden,再捕获 EntitiesForbidden;其余解析错误统一返回 400。生产环境还应在网关或应用内增加鉴权、限流和请求体大小限制,不要仅依赖 Flask 自身。

运行方法

python -m venv .venv
source .venv/bin/activate
pip install flask defusedxml
python app.py

服务默认监听 127.0.0.1:5000。另开终端准备上传验证。

XXE 验证 payload

创建 xxe.xml,其中外部实体指向占位路径:

<?xml version='1.0'?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM 'file:///etc/passwd'>
]>
<root>&xxe;</root>

发送请求:

curl -i -F 'file=@xxe.xml;type=application/xml' http://127.0.0.1:5000/upload-xml

预期返回 HTTP 400,响应 JSON 中的 error 为 DTD forbidden,且不会读取该路径内容。由于本方案在 DTD 阶段就拒绝,外部实体不会进入实体解析。

实体扩展验证 payload 如下,同样应返回 400:

<?xml version='1.0'?>
<!DOCTYPE lolz [
  <!ENTITY lol 'lol'>
  <!ENTITY lol1 '&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;'>
]>
<root>&lol1;</root>

边界与限制

  • defusedxml 解决的是 XML 解析阶段的 DTD、实体和外部实体风险,不替代文件大小限制、请求频率限制、身份认证和业务字段校验。
  • 如果业务确实需要 DTD,应单独进行安全评审,不建议对用户上传内容直接放开;本方案默认全部拒绝。
  • 解析成功后仍应限制元素数量或深度,或将 XML 转为白名单结构,避免合法但庞大的 XML 造成业务层资源消耗。
  • 错误响应保持简短,日志中记录请求标识和异常类型即可,不记录敏感文件路径或完整 XML。
  • 依赖应通过包管理工具安装并锁定版本,关注安全更新;不要从不可信来源复制解析器配置。

对用户上传 XML 的接口,优先选择默认拒绝 DTD 和实体的解析方式,再叠加输入大小与业务校验,能显著降低 XXE 与实体扩展类风险。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。