Django Admin 的自动发现机制(Autodiscover)在处理 URL 路径时,默认会将所有注册的 Model 映射到一个可预测的端点。绝大多数项目上线后,管理后台的入口直接挂在 /admin/ 下,这本身不是问题,问题出在路径的拼接逻辑上。当你在 admin.py 中注册了一个名为 UserProfile 的模型,Django 会按照小写转换规则生成 /admin/app_label/userprofile/ 的路径。攻击者不需要猜解,只要通过报错信息或者简单的字典扫描,就能把后台暴露的模型结构摸得一清二楚。更棘手的是,如果你使用了第三方包如 django-import-export 或自定义的 Admin 视图,这些路径往往不在常规扫描器的字典里,反而成了隐蔽的入口,权限控制一旦没跟上,数据导出接口就变成了脱库的后门。
很多人以为改了 admin.site.site_url 或者重写了 get_urls() 就能隐藏后台,这其实是一种心理安慰。Django Admin 的 URL 分发是基于 ModelAdmin 的 get_urls() 方法层层叠加的。当你自定义了 list_display 中的可点击链接,或者添加了自定义动作(Actions)的跳转,Django 会在根路径下生成带有主键或查询参数的复杂 URL。问题在于,这些 URL 的名称(name)是全局注册的,如果你在多个应用中使用了同名的 Admin 类,后注册的会覆盖前面的,导致路径指向发生混淆。攻击者可以通过故意构造不存在的 app_label 和 model_name 组合,触发 NoReverseMatch 异常,从而在调试模式下直接看到所有已注册的 URL 映射表。即便关闭了 Debug,精心构造的 404 响应时间差异也能被用来侧信道推测哪些模型存在。
给 Django Admin 加一层 OTP 验证已经是很多项目的标配,但多数实现只做到了“登录时验证”,没有做到“会话中验证”。一旦攻击者通过 XSS 或 Session 劫持拿到了一个已通过 OTP 的会话 Cookie,他就能直接访问所有 Admin 页面。真正的二次验证应该基于敏感操作触发。比如,当你访问 /admin/auth/user/ 查看用户列表时,系统只验证了登录态,但当你点击某个用户的密码修改页,或者批量导出数据时,系统应该再次要求输入验证码或硬件密钥。Django 的 ModelAdmin 提供了 change_view()、changelist_view() 等钩子,你可以在这些方法内部插入一个装饰器,检查当前会话是否已经通过了“高敏感操作验证”。如果没通过,直接重定向到一个二次确认页面,而不是继续执行逻辑。
不要直接在 urls.py 里写 admin.site.urls,而是继承 AdminSite 类,重写 admin_view() 方法。在这个方法里,你可以对传入的 request.path 做一层正则匹配,如果路径中包含某些敏感模型名称,但请求来源不符合预期(比如缺少特定的 HTTP 头或参数),直接返回 404。这样做的好处是,即便攻击者猜到了真实路径,没有携带你预设的混淆参数,也看不到任何内容。同时,利用 Django 的 URL 命名空间,给每个 ModelAdmin 手动指定 url_name,避免使用默认的 %s_%s_changelist 这种格式。例如,把 User 模型的列表页命名为 system_audit_log,让 URL 名称与实际模型完全脱钩。这样即使有人通过 {% url 'admin:auth_user_changelist' %} 尝试反向解析,也会因为名称不存在而失败。
下面这段代码展示了如何在 ModelAdmin 中嵌入一个基于时间窗口的二次验证检查器。它不依赖外部包,直接利用 Django 的缓存框架和会话机制。
from django.contrib import admin
from django.core.cache import cache
from django.shortcuts import redirect
from django.urls import path
from functools import wraps
def sensitive_action_required(view_func):
"""装饰器:要求用户在最近 5 分钟内完成过二次验证"""
@wraps(view_func)
def wrapper(self, request, *args, kwargs):
session_key = f"2fa_verified_{request.session.session_key}"
if not cache.get(session_key):
# 重定向到一个轻量级的二次验证确认页
return redirect('admin:2fa_confirm')
return view_func(self, request, *args, kwargs)
return wrapper
class SensitiveModelAdmin(admin.ModelAdmin):
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path('export/',
self.admin_site.admin_view(sensitive_action_required(self.export_view)),
name='export_data'),
]
return custom_urls + urls
def export_view(self, request):
# 执行导出逻辑
pass
注意,这里把验证状态存到了服务端缓存而非仅仅依赖 Session。因为 Session 在登出后可能被复用,而缓存可以设置绝对过期时间。二次验证确认页本身应该是一个极简的 Form,只要求输入一个 6 位数字验证码,验证通过后在缓存中写入标记,有效期设为 5 分钟。这样即便攻击者劫持了会话,只要他无法在 5 分钟内连续通过两次验证,就无法触发导出等高危操作。
对抗自动化扫描:在 Admin 登录页注入动态 Token很多扫描器会直接对 /admin/login/ 发起 POST 请求,尝试弱口令。你可以在 AdminSite 的 login() 方法里,给模板上下文注入一个由服务端生成的动态 Token,这个 Token 必须作为隐藏字段随登录表单一起提交。Token 的生成可以绑定客户端 IP 和 User-Agent 的哈希,有效期 60 秒。这能直接废掉那些不解析 JavaScript、不维持 Cookie 的自动化工具。具体做法是重写 AdminSite.login(),在 GET 请求时生成 Token 存入缓存,在 POST 请求时先校验 Token 是否存在且匹配,不匹配则直接返回 400 错误,根本不走用户名密码验证流程。这层防护放在最前面,能过滤掉 90% 以上的噪音流量。
不要把所有 Model 都无脑注册到 Admin 里。对于包含敏感字段的表,应该创建代理模型(Proxy Model),只暴露脱敏后的字段。更进一步,可以故意注册几个“假模型”,它们的 has_view_permission() 始终返回 False,但在 URL 映射中却真实存在。当攻击者扫描路径时,会发现这些模型端点,但访问时永远返回 403。这能有效迷惑攻击者,让他们在无价值的端点上浪费时间,同时你的日志系统可以监控对这些假模型的访问,一旦有人触碰,立即触发告警。这种蜜罐思路比单纯隐藏路径更主动。
Django Admin 自带的 LogEntry 只记录动作,不记录访问。你需要利用 ModelAdmin 的 changelist_view() 和 change_view() 钩子,在视图执行前记录访问者的 IP、请求路径、User-Agent 和时间戳。如果同一个 IP 在短时间内访问了多个不存在的 Admin 路径,或者尝试了多个不同的模型端点,就应该触发临时封禁。封禁逻辑可以写在中间件里,检查一个基于缓存的 IP 黑名单。当某个 IP 的 404 计数在 10 秒内超过 5 次,就将其加入黑名单,缓存过期时间设为 30 分钟。这种简单的频控能有效阻断路径爆破行为。
如果你不需要通过公网访问 Admin,最稳妥的办法是通过中间件限制来源 IP,只允许内网或特定堡垒机访问。但如果你必须开放公网访问,可以考虑把 Admin 挂载到一个完全随机的路径下,比如 /d5a2f9c1-admin/,并且这个路径不通过版本控制明文存储,而是在部署时通过环境变量注入。同时,在 Web 服务器层(如 Nginx)配置一条规则,对任何访问 /admin 的请求直接返回 444(关闭连接不响应),让扫描器误以为端口未开放。Django 内部的 ADMIN_URL 环境变量只在 urls.py 中引用一次,其他地方全部通过 reverse() 或 {% url %} 动态生成链接,避免在模板或 JavaScript 中硬编码后台路径。
对于高价值目标,仅靠 TOTP 已经不够。Django 生态中已有 django-mfa2 等包支持 WebAuthn,你可以要求管理员在修改关键配置时必须插入硬件密钥并触摸确认。如果没有硬件密钥,可以退而求其次,利用行为特征做二次验证。比如,在 Admin 的基类模板中嵌入一段 JavaScript,记录用户鼠标移动轨迹和按键间隔,将这些数据发回服务端做简单的模式匹配。如果当前操作者的行为特征与历史基线偏差过大,即使 Session 和 OTP 都正确,也拒绝执行敏感操作并强制重新登录。这种无感的持续验证,比单点二次验证要安全得多。
路径混淆和二次验证从来不是孤立的安全措施。路径混淆提高了攻击者的信息收集成本,二次验证则是在假设路径已经暴露的前提下,给核心数据加上最后一道锁。两者结合,再辅以动态 Token、假模型蜜罐、行为特征分析,才能把 Django Admin 从一个高风险暴露面变成一个可控的、具备主动防御能力的管理入口。安全配置没有一劳永逸,定期审计 Admin 中注册的模型列表,删除不再使用的自定义视图,检查 get_urls() 中是否有未受保护的端点,这些基础运维动作比任何高级技巧都重要。
