bigywater.tech
Django · 122

ALLOWED_HOSTS 的校验过程

ALLOWED_HOSTS 常被当成「上线要填的一项」。这篇把它的匹配规则一条条跑出来:端口、大小写、子域名、空列表、非法格式、X-Forwarded-Host,最后看不校验时 Host 头会把什么东西污染掉。

'.example.com'
匹配域名本身和任意层子域名;evilexample.com 不匹配
DEBUG=False + 空列表
所有请求都是 400,包括 localhost
'*'
build_absolute_uri() 会使用攻击者给的 Host

精确匹配

实测 · 本机执行输出
  Host: example.com                → 200  host=example.com abs=http://example.com/reset/abc/
  Host: example.com:8000           → 200  host=example.com:8000 abs=http://example.com:8000/reset/abc/
  Host: EXAMPLE.com                → 200  host=EXAMPLE.com abs=http://EXAMPLE.com/reset/abc/
  Host: www.example.com            → 400  [页面标题: Bad Request (400)]
  Host: example.com.evil.com       → 400  [页面标题: Bad Request (400)]
  Host: evil.com                   → 400  [页面标题: Bad Request (400)]

子域名写法

实测 · 本机执行输出
  Host: example.com                → 200  host=example.com abs=http://example.com/reset/abc/
  Host: a.example.com              → 200  host=a.example.com abs=http://a.example.com/reset/abc/
  Host: a.b.example.com            → 200  host=a.b.example.com abs=http://a.b.example.com/reset/abc/
  Host: evilexample.com            → 400  [页面标题: Bad Request (400)]
  Host: example.com.evil.com       → 400  [页面标题: Bad Request (400)]

空列表

实测 · 本机执行输出
DEBUG = True
  Host: localhost                  → 200  host=localhost abs=http://localhost/reset/abc/
  Host: 127.0.0.1                  → 200  host=127.0.0.1 abs=http://127.0.0.1/reset/abc/
  Host: [::1]                      → 200  host=[::1] abs=http://[::1]/reset/abc/
  Host: testserver                 → 400  [页面标题: DisallowedHost
          at /]
  Host: evil.com                   → 400  [页面标题: DisallowedHost
          at /]
DEBUG = False
  Host: localhost                  → 400  [页面标题: Bad Request (400)]
  Host: 127.0.0.1                  → 400  [页面标题: Bad Request (400)]
  Host: evil.com                   → 400  [页面标题: Bad Request (400)]

格式非法的 Host

实测 · 本机执行输出
  Host: exa mple.com               → 400  [页面标题: Bad Request (400)]
  Host: example.com/evil           → 400  [页面标题: Bad Request (400)]
  Host: example.com@evil.com       → 400  [页面标题: Bad Request (400)]
  Host: example.com:abc            → 400  [页面标题: Bad Request (400)]
  Host:                            → 400  [页面标题: Bad Request (400)]

格式校验与 ALLOWED_HOSTS 是两层:先检查主机名语法,再检查是否在列表里。

X-Forwarded-Host

实测 · 本机执行输出
  [USE_X_FORWARDED_HOST = False]
  只有 Host                                        → 200  host=example.com abs=http://example.com/reset/abc/
  Host + X-Forwarded-Host: proxy.example.com     → 200  host=example.com abs=http://example.com/reset/abc/
  Host + X-Forwarded-Host: evil.com              → 200  host=example.com abs=http://example.com/reset/abc/
  [USE_X_FORWARDED_HOST = True]
  只有 Host                                        → 200  host=example.com abs=http://example.com/reset/abc/
  Host + X-Forwarded-Host: proxy.example.com     → 200  host=proxy.example.com abs=http://proxy.example.com/reset/ab
  Host + X-Forwarded-Host: evil.com              → 400  (400 页面)

只有反向代理真的会设置这个头、并且会清除客户端自己带来的同名头时,才应该开 USE_X_FORWARDED_HOST;这是设计上的提醒,本文没有测试代理。

不校验会怎样

实测 · 本机执行输出
  ALLOWED_HOSTS=["*"]      Host: evil.example → 200 host=evil.example abs=http://evil.example/reset/abc/
  ALLOWED_HOSTS=["example.com"] Host: evil.example → 400

这是在自己的测试项目里用自己的 Host 头请求得到的现象:Django 用 Host 头拼出绝对 URL,如果它可以被任意指定,密码重置邮件之类的链接就可能指向别的域名。

分步演示

ALLOWED_HOSTS 的规则
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7 / Django 6.1.1(默认用 SQLite,其它数据库会在文中注明),用 django.test.Client 或真实的本机服务器进程实测;计时用 time.perf_counter,数字每次运行会有小幅抖动。django.test.Client 用 HTTP_HOST 指定主机名;400 页面只看标题
    • 没有验证:真实反向代理(nginx)对 Host 的改写、IDN / punycode 域名、IPv6 带端口的写法、ALLOWED_HOSTS 与 CSRF_TRUSTED_ORIGINS 的交互(第 123 篇)
    京ICP备2026067502号-1京公网安备11010502063487号