Django 生产环境 DisallowedHost 突然爆发ALLOWED_HOSTS[*] 为什么没用TL;DR你的 Django 项目突然开始报DisallowedHost日志里 Host 是一个奇怪的下划线_而你明明设了ALLOWED_HOSTS [*]。问题出在 Django 的 Host 校验分两步——先做 RFC 1034/1035 域名合法性校验通过才查 ALLOWED_HOSTS。_不是合法域名连第一关都过不去。根治方案一个 15 行的中间件在 CommonMiddleware 之前拦截。1. 事故现场某天早上含光博客的日志/邮件告警里出现了这样一个错误关键信息已标注DisallowedHostat/InvalidHTTP_HOSTheader:_.ThedomainnameprovidedisnotvalidaccordingtoRFC1034/1035.# ⬇ 三行关键证据HTTP_HOST_HTTP_USER_AGENTHello from Palo Alto Networks, find out more about our scans...HTTP_X_FORWARDED_FOR203.0.113.1ALLOWED_HOSTS[*,127.0.0.1,example.com]# ← 明明设了通配符第一反应ALLOWED_HOSTS [*]不是应该放行所有 Host 吗怎么还报 DisallowedHost2. 根因Django 的 Host 校验是两步不是一步翻 Django 源码django/http/request.pyget_host()方法的逻辑是defget_host(self):hostself._get_raw_host()# 从 HTTP_HOST / SERVER_NAME 取# 第一步RFC 1034/1035 域名合法性校验host,portsplit_domain_port(host)ifhostandnothost_validation_re.match(host):raiseDisallowedHost(fInvalid HTTP_HOST header:{host!r}.)# 第二步ALLOWED_HOSTS 白名单比对ifnotvalidate_host(host,settings.ALLOWED_HOSTS):raiseDisallowedHost(fInvalid HTTP_HOST header:{host!r}.)returnhost关键发现第一步的host_validation_re正则在对ALLOWED_HOSTS生效之前就已经跑了。_为什么通不过这个正则因为 RFC 1034/1035 规定域名只能包含字母、数字和连字符-_下划线不在合法字符集中。所以_直接在第一关被毙根本走不到第二步的通配符*。Host 值第一步RFC 校验第二步ALLOWED_HOSTS结果www.baidu.com✅✅200_.com❌含_不会走到DisallowedHost纯数字 IP1.2.3.4✅✅200单下划线_❌不会走到DisallowedHostexample.com✅✅200ALLOWED_HOSTS [*]保证的是第二步永远通过但救不了第一步的 RFC 合规性检查。3. 这些畸形 Host 从哪来互联网扫描器Shodan、Censys、Palo Alto Cortex Xpanse 等在持续探测公网资产。它们发请求时可能会设Host: 你的IP用 IP 而非域名设Host: _占位符/探测标记设Host: scriptalert(1)/scriptXSS 探测完全不发 Host 头某些 HTTP/1.0 客户端这些都不是攻击——只是常规资产扫描。但 Django 默认会为每个 DisallowedHost 记录 traceback 发邮件给 ADMINS日志和邮箱很快会被撑满。4. 解决方案对比方案拦截点副作用推荐度方案 ADjango 自定义中间件应用层Django无⭐⭐⭐⭐⭐方案 BNginx/Caddy 过滤反向代理层需 Web Server 配合⭐⭐⭐⭐方案 C修改 ALLOWED_HOSTS配置治标不治本见第 2 节⭐方案 D忽略日志无核弹级——真正的攻击 Host 注入也会被掩盖❌推荐方案 A原因是 1. 不依赖 Web Server含光博客的 Nginx 是 frp 穿透进来的直接配 Nginx 有坑 2. 15 行代码无外部依赖 3. 精确控制——只拦截畸形 Host正常请求零影响5. 实现BlockBadHostMiddleware在 Django 项目的middleware.py或任何 middleware 文件中添加importrefromdjango.httpimportHttpResponse# RFC 1034/1035 合法域名正则# 匹配example.com / sub.example.com / localhost / 纯 IPv4# 不匹配_ / _something / -bad.com_HOST_REre.compile(r^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?r(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$)classBlockBadHostMiddleware:在 CommonMiddleware 之前拦截无效 Host避免 DisallowedHost tracebackdef__init__(self,get_response):self.get_responseget_responsedef__call__(self,request):hostrequest.META.get(HTTP_HOST,)ifhostandnot_HOST_RE.match(host.split(:)[0]):returnHttpResponse(Bad Request,status400)returnself.get_response(request)然后在settings.py的MIDDLEWARE列表最前面插入MIDDLEWARE[yourapp.middleware.BlockBadHostMiddleware,# ← 必须第一位django.middleware.security.SecurityMiddleware,# ... 其他中间件]为什么必须第一位Django 中间件按列表顺序执行。CommonMiddleware包含get_host()调用如果在BlockBadHostMiddleware之前执行拦截器就没机会跑了。6. 验证# 畸形 Host → 400$curl-s-o/dev/null-w%{http_code}http://127.0.0.1:8770/-HHost: _400# 正常域名 → 200$curl-s-o/dev/null-w%{http_code}http://127.0.0.1:8770/-HHost: www.baidu.com200畸形 Host 被静默拦截为 400不再触发 traceback 和邮件告警。正常请求零影响。7. 延伸为什么 DNS 允许下划线但 HTTP Host 不允许这里有一个容易混淆的点DNS 层面SRV 记录_http._tcp.example.com和 DKIM/SPF 等确实使用下划线RFC 2181 明确说DNS 不对 label 内容做限制HTTP Host 头层面RFC 952 RFC 1123主机名规范禁止下划线。Django 的host_validation_re遵循的是主机名规范不是 DNS 规范所以_dmarc.example.com作为 DNS 记录是合法的但作为 HTTP Host 头传给 Django 就会被拒绝——除非你在前面放了这个中间件。8. 总结你看到的真实原因修法DisallowedHost: _扫描器发的畸形 Host自定义中间件返回 400 而不是抛异常ALLOWED_HOSTS[*]却没用*只管第二步管不了第一步 RFC 校验中间件插在 MIDDLEWARE 第一位日志/邮箱被撑满每个畸形请求都触发 traceback 邮件拦截后只返回 400无 traceback这个错误在生产环境非常常见——任何暴露在公网的 Django 项目都会被扫描器光顾。上述 15 行中间件可以永久解决这个问题。本文代码已在 Django 5.2 Gunicorn 生产环境中验证通过。如果你也有类似的排查经验欢迎在评论区交流。