icvoss/django-waf
GitHub: icvoss/django-waf
为 Django 应用提供自托管 WAF、bot 管理与表单保护的安全中间件,无需依赖第三方反向代理即可实现多层请求过滤与自动化威胁检测。
Stars: 3 | Forks: 0
# django-waf
为 Django 提供的自托管请求过滤、bot 管理和 WAF middleware。
提供双层防御(nginx + Django middleware),具备 rate limiting、
user-agent 异常评分、JS proof-of-work challenge、基于 path 的威胁
评分、nginx blocklist 生成以及集体威胁情报源集成 —
所有这些均可无需反向代理供应商即可进行配置。
## 功能
- **Rate limiting** — 基于 IP 的滑动窗口限制(burst、per-minute、per-5-min)
- **UA 异常评分** — 启发式检测不可能的 OS/浏览器组合、
古老版本、scraper 库
- **基于 path 的威胁评分** — 对凭据探测(
`.env`, `wp-config`, AWS/SSH 配置文件)的可疑 path 检测会增加异常评分
- **HTTP 方法过滤** — 在规则评估前拦截非标准方法(例如 `HEAD`, `OPTIONS`,
`PUT`, `PATCH`, `DELETE`)
- **JS proof-of-work challenge** — 针对可疑客户端的 hashcash 风格 SHA-256 challenge
(无需 CAPTCHAs,无需第三方依赖)
- **Challenge 自动升级** — 超过未解决 challenge 阈值的重复违规者将被自动拦截一段可配置的 TTL 时间
- **无 Referer challenge 触发器** — 可选择对缺少 `Referer` header 的直接导航请求发起 challenge
- **GeoIP 国家代码填充** — 使用 MaxMind GeoLite2 数据库将 ISO 国家代码附加到请求日志
条目中
- **复合规则** — 结合 UA 模式与 IP/CIDR 的拦截规则
- **进程内规则缓存** — 经过版本检查的内存缓存避免了每个请求的 Redis 往返;
在规则更改时自动失效
- **命中计数跟踪** — 拦截规则会累积命中计数,并定期刷新到
数据库
- **可配置的异常评分阈值** — 为日志、
challenge 和 block 判决设置独立的阈值
- **nginx blocklist 生成** — 导出 `map`/`geo` 块,用于在 < 0.01 ms 的延迟下进行 C 级别
过滤
- **异常检测** — 为 UA 轮换、子网
突发和 challenge 农场自动创建过期规则
- **集体威胁情报源** — 可选同步跨部署的匿名化威胁情报
- **员工仪表盘** — 基于 HTMX 的实时分析,带有异常管理功能
- **表单保护** — 表单层的深度防御:签名渲染
token、honeypots、time-trap、UA 一致性、JS-touch、凭据
节流(防止枚举)、注册频率、每次提交的 PoW。
Mixin / decorator / template-tag 入口点;针对单独表单的配置;
可选的 challenge 重放,用于挽救误报
- **Fail-open 设计** — Redis 宕机永远不会破坏网站
## 环境要求
- Python >= 3.11
- Django >= 5.2
- Redis(通过 `django-redis >= 5.4`)
- `httpx >= 0.27`(用于威胁情报源同步)
- 可选:`celery >= 5.3`(用于计划任务)
- 可选:`maxminddb >= 2.4`(用于 GeoIP 查找)
## 安装说明
```
pip install django-waf
```
安装可选附加组件:
```
pip install django-waf[geoip] # adds maxminddb for GeoIP support
pip install django-waf[celery] # adds celery for scheduled tasks
```
添加到 `INSTALLED_APPS`:
```
INSTALLED_APPS = [
# ...
"django_waf",
]
```
添加 middleware — 请将其放在 `SecurityMiddleware` **之后**以及
其他 middleware **之前**,以便它能尽早拦截请求:
```
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django_waf.middleware.WafMiddleware", # <-- here
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
# ...
]
```
为 challenge 流程和员工仪表盘引入 URL 路由:
```
# urls.py
from django.urls import include, path
urlpatterns = [
path("waf/", include("django_waf.urls")),
# ...
]
```
配置 Redis cache backend(rate limiting 所需):
```
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/0",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
}
}
```
运行 migrations:
```
python manage.py migrate django_waf
```
## 配置参考
所有配置均位于 `DJANGO_WAF_*` 命名空间下,并具有合理的默认值。
### 核心
| 配置项 | 默认值 | 描述 |
|---------|---------|-------------|
| `DJANGO_WAF_ENABLED` | `True` | 主开关 — 禁用后将放行所有请求 |
| `DJANGO_WAF_EXEMPT_PATHS` | `["/static/", "/media/", "/health/", "/favicon.ico"]` | 完全绕过 WAF 评估的 URL 前缀 |
| `DJANGO_WAF_EXEMPT_HOSTS` | `[]` | 完全绕过 WAF 评估的主机名。支持精确匹配,或以点开头(`.example.com`)匹配域名及任意子域名(类似于 Django 的 `ALLOWED_HOSTS`)。匹配前会去除端口号 |
| `DJANGO_WAF_TRUST_X_FORWARDED_FOR` | `False` | 信任 `X-Forwarded-For` header 以提取客户端 IP |
| `DJANGO_WAF_REDIS_ALIAS` | `"default"` | 用于 Redis 连接的 Django cache alias |
| `DJANGO_WAF_ALLOWED_METHODS` | `None` | 允许的 HTTP 方法;包含其他方法的请求将在规则评估前收到 405。`None` 表示允许所有方法。 |
### Rate Limiting
| 配置项 | 默认值 | 描述 |
|---------|---------|-------------|
| `DJANGO_WAF_RATE_LIMIT_BURST` | `10` | 每个 IP 每秒最大请求数 |
| `DJANGO_WAF_RATE_LIMIT_PER_MINUTE` | `120` | 每个 IP 每分钟最大请求数 |
| `DJANGO_WAF_RATE_LIMIT_PER_5MIN` | `600` | 每个 IP 每 5 分钟最大请求数 |
### Challenge
| 配置项 | 默认值 | 描述 |
|---------|---------|-------------|
| `DJANGO_WAF_CHALLENGE_DIFFICULTY` | `20` | 当未设置桌面端/移动端覆盖配置时的后备 proof-of-work 前导零 **位数**。平均工作量为 `2 ** bits` 次 SHA-256 哈希计算 |
| `DJANGO_WAF_CHALLENGE_DIFFICULTY_DESKTOP` | `22` | 非移动端 User-Agent 的 PoW 难度(位数)。约 4M 次哈希,在笔记本电脑上约需 1–2 秒 |
| `DJANGO_WAF_CHALLENGE_DIFFICULTY_MOBILE` | `18` | 移动端 User-Agent 的 PoW 难度(位数)。约 260k 次哈希,在低端手机上约需 1–3 秒 |
| `DJANGO_WAF_CHALLENGE_URL` | `""` | 指向 challenge 视图的可选字面量 path。在使用按请求 urlconf 路由(django-hosts 及类似项目)且 `reverse("django_waf:challenge")` 无法解析的项目中设置此项。留空 = 使用 `reverse()` |
| `DJANGO_WAF_VERIFY_URL` | `""` | 指向验证视图的可选字面量 path。留空 = 使用 `reverse()` |
| `DJANGO_WAF_CHALLENGE_COOKIE_TTL` | `86400` | 已解决 challenge 的 cookie 保持有效的秒数 |
| `DJANGO_WAF_CHALLENGE_NO_REFERER` | `False` | 对没有 `Referer` header 的请求发起 challenge |
| `DJANGO_WAF_NO_REFERER_EXEMPT_PATHS` | `["/", "/search/", "/robots.txt", "/sitemap.xml", "/favicon.ico"]` | 豁免无 referer challenge 的路径(仅在 `DJANGO_WAF_CHALLENGE_NO_REFERER` 为 `True` 时评估) |
| `DJANGO_WAF_CHALLENGE_ESCALATION_THRESHOLD` | `10` | 在自动升级为 block 之前的未解决 challenge 次数 |
| `DJANGO_WAF_ESCALATION_BLOCK_TTL` | `3600` | 升级 block 的 TTL(秒) |
### 异常评分
| 配置项 | 默认值 | 描述 |
|---------|---------|-------------|
| `DJANGO_WAF_SCORE_THRESHOLD_LOG` | `3.0` | 请求被记录时的异常评分 |
| `DJANGO_WAF_SCORE_THRESHOLD_CHALLENGE` | `5.0` | 发出 challenge 时的异常评分 |
| `DJANGO_WAF_SCORE_THRESHOLD_BLOCK` | `7.0` | 请求被 block 时的异常评分 |
| `DJANGO_WAF_ANOMALY_THRESHOLD_DISTINCT_UAS` | `20` | 触发 UA 轮换异常前,每个 IP 的不同 UA 数量 |
| `DJANGO_WAF_AUTO_RULE_EXPIRY_HOURS` | `24` | 自动生成的规则过期前的小时数 |
| `DJANGO_WAF_SUSPICIOUS_PATH_PATTERNS` | `[r"\.env", r"wp-config\.php", ...]` | 可疑路径(凭据探测、配置文件)的正则表达式模式;匹配的路径将把 `DJANGO_WAF_SUSPICIOUS_PATH_SCORE` 添加到异常评分中 |
| `DJANGO_WAF_SUSPICIOUS_PATH_SCORE` | `3.0` | 每次可疑路径匹配增加的分数 |
### 日志记录
| 配置项 | 默认值 | 描述 |
|---------|---------|-------------|
| `DJANGO_WAF_LOG_SAMPLE_RATE` | `0.01` | 被记录的已允许请求的比例 (0.0–1.0) |
| `DJANGO_WAF_LOG_RETENTION_DAYS` | `30` | 保留 `RequestLog` 条目的天数 |
### GeoIP
| 配置项 | 默认值 | 描述 |
|---------|---------|-------------|
| `DJANGO_WAF_GEOIP_PATH` | `None` | MaxMind GeoLite2-Country `.mmdb` 数据库的文件系统路径。`None` 表示禁用 GeoIP。 |
### nginx 集成
| 配置项 | 默认值 | 描述 |
|---------|---------|-------------|
| `DJANGO_WAF_NGINX_BLOCKLIST_PATH` | `"/etc/nginx/conf.d/django-waf-blocklist.conf"` | 生成的 nginx blocklist 的输出路径 |
| `DJANGO_WAF_ACCESS_LOG_PATH` | `"/var/log/nginx/access.log"` | 用于解析的 nginx 访问日志路径 |
| `DJANGO_WAF_NGINX_RELOAD_COMMAND` | `["nginx", "-s", "reload"]` | 生成 blocklist 后重新加载 nginx 的命令 |
### 集体威胁情报源
| 配置项 | 默认值 | 描述 |
|---------|---------|-------------|
| `DJANGO_WAF_FEED_ENABLED` | `True` | 启用集体威胁情报源同步 |
| `DJANGO_WAF_FEED_URL` | `"https://threats.icv.dev/v1/feed.json"` | 威胁情报源 JSON endpoint |
| `DJANGO_WAF_FEED_MIN_CONFIDENCE` | `0.8` | 将情报源条目导入为规则所需的最低置信度 (0.0–1.0) |
| `DJANGO_WAF_FEED_REPORT` | `False` | 将本地检测情况上报给情报源(可选加入) |
| `DJANGO_WAF_FEED_REPORT_URL` | `"https://threats.icv.dev/v1/report"` | 遥测报告 endpoint |
| `DJANGO_WAF_FEED_API_KEY` | `""` | 用于情报源身份验证的 API key |
### 表单保护 (v0.11.0)
表单保护子系统**针对单个表单可选加入**。在表单通过 mixin、decorator 或 template tag 加入之前,默认配置是不起作用的。
| 配置项 | 默认值 | 描述 |
|---------|---------|-------------|
| `DJANGO_WAF_SIGNING_KEY` | `""` | 全包范围的 HMAC 密钥。与 Django 的 `SECRET_KEY` 分离,因此轮换生命周期相互独立。留空 → 从 `SECRET_KEY` 派生,并且 `django_waf.W003` 会在启动时发出警告 |
| `DJANGO_WAF_FORM_PROTECTION_ENABLED` | `True` | 主关闭开关。设为 `False` 会使 mixin/decorator/tag 直接短路放行,而不运行防御机制 |
| `DJANGO_WAF_FORM_FLAG_THRESHOLD` | `2.0` | 超过此聚合分数将触发 FLAGGED |
| `DJANGO_WAF_FORM_BLOCK_THRESHOLD` | `5.0` | 超过此聚合分数将触发 BLOCKED |
| `DJANGO_WAF_FORM_CHALLENGE_ON_FLAG` | `True` | 将 FLAGGED 的提交重定向到 `/waf/challenge/`(然后重放 POST)。`False` 返回通用的拒绝响应 |
| `DJANGO_WAF_FORM_EMIT_PASSED_SIGNAL` | `False` | 在每次 PASS 时触发 `form_submission_passed`。默认关闭 — 繁忙的站点会在热路径上消耗大量周期;结构化日志已经记录了(采样的)通过情况 |
| `DJANGO_WAF_FORM_TOKEN_TTL` | `3600` | 渲染 token 的生命周期(秒);同时也是 Redis marker 的 TTL |
| `DJANGO_WAF_FORM_HONEYPOT_FIELD_NAMES` | `["url", "website", "homepage", "email_confirm"]` | 为每个表单轮换 honeypot 字段提供的名称池 |
| `DJANGO_WAF_FORM_TRAP_MIN_SECONDS` | `1.5` | 低于此值 → 标记;低于 0.5 → 拦截(硬性下限) |
| `DJANGO_WAF_FORM_TIME_TRAP_MAX_SECONDS` | `3600` | 高于此值 → 标记(陈旧的表单) |
| `DJANGO_WAF_FORM_CREDENTIAL_THROTTLE_WINDOW` | `900` | 凭据失败计数器的滑动窗口(秒) |
| `DJANGO_WAF_FORM_CREDENTIAL_THROTTLE_LIMIT` | `5` | 每个账户的阈值。仅用于观察(驱动 `credential_attack_observed` signal);用户永远不可见 |
| `DJANGO_WAF_FORM_CREDENTIAL_IP_LIMIT` | `20` | 每个 IP 的阈值。**驱动用户可见的 challenge** — 无论尝试了哪个账户,行为都一致(防止枚举) |
| `DJANGO_WAF_FORM_SIGNUP_VELOCITY_WINDOW` | `86400` | 已完成注册计数器的窗口(24小时) |
| `DJANGO_WAF_FORM_SIGNUP_VELOCITY_LIMIT` | `5` | 标记下一次尝试前每个 IP 的成功注册数 |
| `DJANGO_WAF_FORM_POW_DIFFICULTY` | `12` | 每次提交的 PoW 难度(位数)。12 ≈ 4k 次 SHA-256 哈希 ≈ 桌面端 50ms / 移动端约 200ms |
| `DJANGO_WAF_FORM_REPLAY_STORE` | `"session"` | 存放用于重放的 FLAGGED POST 数据的位置。目前仅实现了 `"session"` |
| `DJANGO_WAF_FORM_DEFENCE_WEIGHTS` | (见代码) | 各项防御的评分权重;可通过 `FormProtection(defence_weights={...})` 针对表单单独覆盖 |
**用法** — Django Form mixin(推荐用于新表单):
```
from django import forms
from django_waf.forms import FormProtection, ProtectedForm
class ContactForm(ProtectedForm, forms.Form):
name = forms.CharField()
email = forms.EmailField()
message = forms.CharField(widget=forms.Textarea)
waf = FormProtection(
form_id="contact",
defences=("render_token", "honeypot", "time_trap", "ua_consistency"),
)
```
在视图中:
```
def contact_view(request):
form = ContactForm(request.POST or None, request=request)
if request.method == "POST" and form.is_valid():
# form.waf_result holds the FormEvaluationResult.
...
```
在模板中:
```
```
**手写 HTML**(绕过 Django 的 Form 层的表单):
```
# views.py
from django_waf.forms import waf_protect_post
@waf_protect_post(form_id="contact-handwritten",
defences=("honeypot", "time_trap"))
def contact_view(request):
if request.method == "POST":
...
```
```
{% load waf_form_tags %}
```
**HTMX 兼容性** — 表单保护渲染 token 在
同一表单的 HTMX 重新渲染期间保持不变(用户修复验证
错误时会保留相同的 token)。支持重放
保护的 Redis marker 仅在 PASS 判决时消耗,因此在
验证错误后连续提交两次也能正常工作。运维人员
必须确保 HTMX 目标在替换的片段中包含 `{{ form.waf_fields }}` /
`{% waf_protect %}`。
**已验证的表单** — 在 `FormProtection` 上设置 `skip_for_authenticated=True`,
以便为已登录用户跳过反垃圾式的防御,
同时保留 `render_token` 以保证完整性:
```
waf = FormProtection(
form_id="team-invite",
defences=("render_token",),
skip_for_authenticated=True,
)
```
## Celery Beat 计划任务
如果使用 Celery,请为自动化任务配置 beat 计划任务:
```
from celery.schedules import crontab
CELERY_BEAT_SCHEDULE = {
"django-waf-flush-rule-hit-counts": {
"task": "django_waf.tasks.flush_rule_hit_counts",
"schedule": crontab(minute="*/5"),
},
"django-waf-generate-blocklist": {
"task": "django_waf.tasks.generate_blocklist",
"schedule": crontab(minute="*/5"),
},
"django-waf-detect-anomalies": {
"task": "django_waf.tasks.detect_anomalies",
"schedule": crontab(minute="*/15"),
},
"django-waf-parse-access-log": {
"task": "django_waf.tasks.parse_access_log",
"schedule": crontab(minute="*/10"),
},
"django-waf-expire-rules": {
"task": "django_waf.tasks.expire_rules",
"schedule": crontab(minute="*/30"),
},
"django-waf-update-ip-reputation": {
"task": "django_waf.tasks.update_ip_reputation",
"schedule": crontab(hour="*/6", minute=0),
},
"django-waf-prune-request-logs": {
"task": "django_waf.tasks.prune_request_logs",
"schedule": crontab(hour=4, minute=0),
},
"django-waf-sync-threat-feed": {
"task": "django_waf.tasks.sync_threat_feed",
"schedule": crontab(hour=4, minute=30),
},
"django-waf-report-threat-telemetry": {
"task": "django_waf.tasks.report_threat_telemetry",
"schedule": crontab(hour=5, minute=0),
},
}
```
## 管理命令
| 命令 | 描述 |
|---------|-------------|
| `django_waf_generate_blocklist` | 生成 nginx blocklist 文件(使用 `--dry-run` 预览) |
| `django_waf_detect_anomalies` | 运行异常检测器并自动创建拦截规则(`--dry-run`) |
| `django_waf_prune_logs` | 删除超过保留期的 `RequestLog` 条目(`--dry-run`) |
| `django_waf_sync_feed` | 从集体威胁情报源获取并导入规则(`--dry-run`) |
## 仪表盘
员工仪表盘位于 `/waf/dashboard/`,供已通过身份验证的员工
用户使用。它提供:
- 实时流量计数器(允许、拦截、challenge、节流)
- 拦截次数排名前 10 的 IP
- 等待审查的自动检测到的异常
超级用户可以直接在异常面板中**确认**自动生成的规则(将其提升为永久规则)或者
**拒绝**它们(将其停用)。
## 架构
```
Client → nginx (C-level blocklist, < 0.01 ms)
→ Django WafMiddleware (dynamic analysis, < 0.5 ms)
→ Application views
```
middleware 按以下顺序评估请求:
1. **豁免 path/host 绕过** — 静态资源、健康检查 endpoint 以及豁免 host 跳过所有评估
2. **HTTP 方法过滤** — 不允许的方法立即收到 405
3. **主开关检查** — `DJANGO_WAF_ENABLED = False` 会放行所有请求
4. **员工/超级用户绕过** — 已验证身份的员工跳过规则评估
5. **有效 challenge cookie 检查** — 对之前已解决的 challenge 予以通过
6. **允许规则 → 拦截规则 → Rate limits** — 显式规则匹配
7. **无 Referer challenge** — 可选地对没有 `Referer` header 的请求发起 challenge
8. **Path 评分(始终进行)+ UA 评分(10 次请求后进行)** — 异常评分
从可疑 path 和 UA 启发式中累积;评分阈值决定
判决结果(记录日志 / challenge / block)
9. **Challenge 升级** — 超过未解决 challenge 阈值的 IP 将被自动拦截 `DJANGO_WAF_ESCALATION_BLOCK_TTL` 秒
10. **判决分发** — 渲染响应(允许 / 拦截 / challenge / 节流)、
写入采样日志,并发出 WAF signal
## 开发
```
# 运行测试
pytest
# 运行带有 coverage 的测试
pytest --cov=src --cov-report=term-missing
# Lint
ruff check src/ tests/
ruff format src/ tests/
# Type check
mypy src/
```
## 许可证
MIT
标签:Bot管理, CISA项目, Django, Nginx, WAF, Web安全, 密码管理, 异常检测, 搜索引擎查询, 流量过滤, 蓝队分析, 运行时操纵, 逆向工具