测试矩阵、安全边界与 CSP
Django 6.0 CSP 配置
# config/settings.py
from django.utils.csp import CSP
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.csp.ContentSecurityPolicyMiddleware",
# 其余 middleware……
]
SECURE_CSP_REPORT_ONLY = {
"default-src": [CSP.SELF],
"img-src": [CSP.SELF, "data:"],
"script-src": [CSP.SELF],
"style-src": [CSP.SELF],
}from django.test import TestCase
class SecurityHeaderTests(TestCase):
def test_csp_report_only_header(self):
response = self.client.get("/health/")
self.assertIn("Content-Security-Policy-Report-Only", response)先使用 report-only 收集真实资源来源,再进入执行模式。CSP 能限制浏览器加载来源,但不能替代模板转义、输出净化、CSRF、权限或上传校验。
测试分层
- 纯函数:
SimpleTestCase或标准库测试,不访问数据库。 - 模型/视图:
TestCase,利用事务隔离与测试客户端。 - 真实事务并发:
TransactionTestCase和目标数据库。 - 异步请求:
AsyncClient,显式 await。 - 部署:独立预发布环境验证代理、TLS、静态资源和进程信号。
深水边界
- 默认测试客户端不执行 CSRF 检查;安全专项使用
Client(enforce_csrf_checks=True)。 - 文件上传大小还要在 Web 服务器/代理限制,尤其 ASGI 请求可能在应用校验前落盘。
check --deploy是配置提示,不是渗透测试或架构审计。- 测试中
force_login()绕过认证流程,登录表单本身仍需独立测试。 - nonce 页面和 CSP Header 必须由同一请求生成;整页缓存可能复用 nonce。
验收
弃用告警、测试与部署检查通过;CSRF 负向测试有效;CSP report-only Header 可观测;敏感数据不会进入响应或日志。
练习(先独立完成)
- 用强制 CSRF 客户端证明无 token 的表单 POST 被拒绝。
- 为 CSP header 写最小断言,并说明为何不固定 nonce 全文。
- 为查询数、越权和日志脱敏各写一个回归测试。
深度加餐:安全测试要攻击边界,不要只测 200
Django 的安全默认值很强,但只有被测试的边界才会留在团队记忆里。
先做一个可观察实验
from django.test import TestCase, Client
from django.urls import reverse
class TicketSecurityTests(TestCase):
def test_anonymous_user_is_redirected(self):
response = Client().get(reverse('ticket-list'))
self.assertEqual(response.status_code, 302)
def test_csp_header_is_present(self):
response = Client().get(reverse('home'))
self.assertIn('Content-Security-Policy', response.headers)预期:匿名访问被导向登录;响应带 CSP 头。再补 403、CSRF 失败、恶意 HTML 和越权读取用例。
原理下潜与工程边界
测试矩阵应覆盖身份 × 方法 × 状态 × 数据边界;CSP 是浏览器执行策略,不替代模板自动转义、CSRF、权限和输出编码。Django 6.0 的安全中间件仍需结合部署代理正确设置。
测试环境关闭 DEBUG 不代表生产配置正确;CSP report-only 只能观察不能阻断;允许 unsafe-inline/unsafe-eval 会显著削弱策略。
反例排错路线
先查看 response.headers,再用 Django test client 模拟恶意输入;安全告警要保留请求路径、用户和规则,但绝不记录密码、token 和完整个人数据。
练习加码(仍不提供答案)
加码:为工单详情写越权读取测试,并把 CSP 从 report-only 逐步收紧到 enforce。
验收时请补充:正常路径、权限或失败路径、以及一条能证明事务/资源/配置边界没有被绕过的测试。
继续查阅:Django 官方文档(外部资料已收录于证据账本) · Django 安全清单(外部资料已收录于证据账本)
课程合同:测试、安全与 CSP
学习目标
学完本篇,读者应能把「测试覆盖匿名、越权、CSRF、CSP 报告和冲突路径」拆成输入、可信边界、Django 机制和可复查证据;同时能说明 Django 6.0 与 5.2 LTS 的差异不会改变业务不变量。
心智模型
请求/数据输入 → 边界检查 → Django 核心机制 → 可观察输出 → 恢复动作。配置、数据库提交和外部副作用分别拥有自己的生命周期;没有证据的“应该可以”不算完成。
本地机制图
图中的“输入”是不可信的 HTTP、表单、任务或配置;“边界”负责身份、格式、权限、大小、超时与版本;“机制”才是 URL、View、ORM、事务、缓存、Tasks 或 ASGI;“证据”是测试、响应、日志、查询计划和发布记录。
最小实验
在全新虚拟环境中运行:
python -m django --version
python manage.py check
python manage.py test实验只验证一个机制:输入一条有效值,再输入一条空值、重复值或无权限值。每次实验都记录 Django 版本、数据库、命令和退出码。
完整场景
把实验接到 tickets 工单流:请求携带用户身份和工单 ID,经过当前专题负责的边界,完成一次读或写,并留下响应状态、数据库变化、日志字段和清理动作。完整场景必须能第二次执行,或明确说明幂等键、事务回滚、缓存失效和任务重试策略。
运行输出
成功路径应出现:
Django 6.0.x
System check identified no issues (0 silenced).
OK失败路径要保留真实信号,例如 400(输入无效)、403(权限/CSRF)、404(资源不存在)、405(方法不允许)或数据库约束异常;不要把失败改写成含糊的 200。
失败边界
至少验证:空输入、重复提交、匿名用户、越权对象、并发写入、下游不可用、超时和版本不兼容。记录“谁拒绝、何时拒绝、状态是否回滚、重试是否安全”。开发模式的 traceback 不得进入生产响应,缓存不得跨用户复用敏感结果。
原理与排错
先确认版本和 settings,再用 manage.py check 定位启动问题;接着确认 URL/任务入口、输入解析和权限;最后检查 SQL、事务提交、响应和副作用。若结果异常,用最小复现打开 SQL/请求 ID/任务 ID 观测,沿「复现 → 缩小 → 观测 → 修复 → 回归」闭环,不靠重装依赖猜测。
练习(正文不泄答案)
- 为上述完整场景补一条正常路径和两条失败路径测试,并记录完整输出。
- 画出本篇机制图,标出身份 owner、数据提交点、缓存/任务副作用和恢复动作。
- 故意制造一次重复、越权或超时,说明应返回的状态、日志字段和是否可重试。
- 写一条 Django 5.2 LTS 兼容验收标准,再说明 Django 6.0 的新能力是否需要条件分支。
验收标准:命令可重复、输出可复查、正文没有练习答案;完成后再打开本阶段答案册。
答案册
本阶段答案册提供参考实现、关键推理、失败边界和替代方案;先独立完成练习再对照。
下一篇
下一篇是09-testing-and-security/02-uploads-logging-dependencies,继续沿同一条输入—边界—机制—证据链前进。