DVFA 代码审计报告
| 项 | 内容 |
|---|---|
| 目标 | DVFA (Damn Vulnerable Flask App) |
| 路径 | /11-code/DVFA |
| 技术栈 | Python Flask, Flask-WTF, SQLite |
| 审计类型 | 代码审计(静态) |
| 审计日期 | 2026-08-16 |
| 应用性质 | 教学靶场,故意包含 OWASP Top 10 漏洞用于安全训练 |
一、概述
DVFA 是一个 故意存在漏洞的 Flask 教学靶场(DVWA 的 Flask 移植版),不是生产系统。
审计目标是:准确盘点其代码中真实存在的漏洞,并给出原因、利用方式与修复建议。
说明(准确性声明):
- 本报告只报代码中真实存在的漏洞,不虚构。
db.py中数据库插入使用参数化查询(?占位符),搜索使用 Python 侧in过滤而非 SQL 拼接,
因此不存在 SQL 注入,特此说明。- 本应用没有登录/会话体系,因此传统意义的认证绕过、CSRF 攻击场景受限,相关条目按实际风险如实描述。
漏洞汇总表
| # | 漏洞 | 风险 | 位置 | 对应 OWASP |
|---|---|---|---|---|
| 1 | 越权(Broken Access Control) | 🔴 高 | routes.py:16-21 |
A01:2021 |
| 2 | SSRF 服务端请求伪造 | 🔴 高 | routes.py:24-40 |
A10:2021 |
| 3 | 存储型 XSS | 🔴 高 | routes.py:50 + guestbook.html:1,50 |
A03:2021 |
| 4 | 反射型 XSS | 🟠 中 | routes.py:44 + guestbook.html:45 |
A03:2021 |
| 5 | 硬编码 SECRET_KEY(CSRF 防护失效) | 🟠 中 | app.py:4 |
A07:2021 |
| 6 | 调试模式开启(Debug Mode) | 🟠 中 | app.py:8 |
信息泄露/潜在 RCE |
| 7 | iframe 嵌入用户可控 URL | 🟡 低 | analyzer.html:15 |
与 #2 关联 |
| 8 | 缺少安全响应头 / 外部 CDN | 🟡 低 | app.py / guestbook.html:14 |
加固建议 |
二、漏洞详情
漏洞 #1:越权(Broken Access Control)🔴 高
漏洞代码原因
routes.py 第 16-21 行:
@app.route('/admin')
def admin():
if(request.cookies.get('admin') == "True"):
return render_template('admin.html')
else:
return render_template('403.html')
根因: 使用客户端完全可控的 Cookie 值 admin=True 作为权限判定依据,且无任何服务端校验。
Cookie 由浏览器/客户端保存并随请求回传,攻击者可以自由改写,因此这条检查形同虚设。
正确做法是基于服务端会话(session)或数据库核验真实用户身份与权限。
利用方式
- 直接访问
GET /admin,默认返回 403。 - 使用 Burp Suite / 浏览器插件 / 开发者工具,在请求头中注入 Cookie:
Cookie: admin=True或执行 JS:
document.cookie = "admin=True"。 - 重新访问
/admin,即进入管理后台(admin.html,一个 phpMyAdmin 登录页 Mockup)。
任何未授权用户均可绕过访问控制进入"管理界面",即 IDOR / 功能级越权。
修复建议
- 禁止用客户端可控数据做权限判断。 改用在服务端 session 中保存登录态:
@app.route('/login') def login(): # 校验账号密码后 session['is_admin'] = True - 权限校验放服务端,通过装饰器/中间件统一处理,例如:
from functools import wraps def admin_required(f): @wraps(f) def wrapper(*args, **kwargs): if not session.get('is_admin'): abort(403) return f(*args, **kwargs) return wrapper @app.route('/admin') @admin_required def admin(): return render_template('admin.html') - 权限在服务端通过数据库(如用户表的
is_admin字段)确认,不从请求中读取。
漏洞 #2:SSRF(服务端请求伪造)🔴 高
漏洞代码原因
routes.py 第 24-40 行:
@app.route('/analyzer')
def follow_url():
url = request.args.get('url', '')
if url:
r = requests.get(url)
return render_template('analyzer.html', req=r)
else:
return render_template('analyzer-empty-state.html')
根因: 用户可控参数 url 未经任何校验直接传给 requests.get() 由服务端发起请求。
服务端通常能访问内网,攻击者可借此探测/访问内网资源、云元数据、本地服务。
代码注释里已经写好了基于 ipaddress 的修复方案,但被注释掉了。
利用方式
请求由服务端发出,可直接访问内网/本机:
# 探测本机端口/服务
/analyzer?url=http://127.0.0.1:80
/analyzer?url=http://127.0.0.1:5000
/analyzer?url=http://localhost:3306
# 云环境元数据服务(AWS/GCP 等)
/analyzer?url=http://169.254.169.254/latest/meta-data/
# 内网横向探测(端口扫描思路)
/analyzer?url=http://10.0.0.1:8080
响应在 analyzer.html 中回显 req.headers 和 iframe 预览,攻击者可据此做内网侦察、端口扫描、读取内网服务内容。
注:标准 requests 库不支持 file:///gopher:// 协议(会抛 InvalidSchema),主要攻击面为 http/https 访问内网。
修复建议
按注释思路做服务端校验,且要注意 DNS Rebinding 与重定向绕过:
import ipaddress
import socket
from urllib.parse import urlparse
@app.route('/analyzer')
def follow_url():
url = request.args.get('url', '')
if url:
parsed = urlparse(url)
if parsed.scheme not in ('http', 'https'):
return render_template('analyzer-empty-state.html')
try:
ip = socket.gethostbyname(parsed.hostname)
except socket.gaierror:
return render_template('analyzer-empty-state.html')
if ipaddress.ip_address(ip).is_private:
return render_template('analyzer-empty-state.html')
# 防重定向绕过:先禁用自动重定向,逐跳校验后再访问
r = requests.get(url, allow_redirects=False)
...
关键点:
- 协议白名单,仅允许
http/https。 - 校验解析后的 IP 是否私有/回环/链路本地(含
169.254.169.254)。 - 用
allow_redirects=False逐跳校验,防重定向绕过。 - 部署时通过防火墙/ACL 限制出网,避免服务器直连内网。
漏洞 #3:存储型 XSS 🔴 高
漏洞代码原因
服务端 routes.py 第 43-54 行,评论原样存储、不过滤:
@app.route('/guestbook', methods=['GET', 'POST'])
def guestbook():
search_query = request.args.get('q')
comments = db.get(search_query)
form = forms.AddCommentForm()
if form.validate_on_submit():
db.add(form.comment.data) # 评论未清洗直接入库
return redirect(url_for('guestbook'))
return render_template('guestbook.html', form=form, comments=comments, search_query=search_query)
模板 guestbook.html 第 1 行关闭了 Jinja2 自动转义:
{% autoescape false %}
第 48-53 行将评论原样输出:
{% for comment, date in comments %}
<div class="comment">
<p class="comment-content">{{ comment }}</p>
...
根因: Flask 的 Jinja2 默认开启 autoescape(HTML 自动转义),此处被 {% autoescape false %} 显式关闭;
加上评论存储时无任何清洗 → 攻击者提交的 HTML/JS 原样渲染给所有访客。
利用方式
- 在留言板提交一条评论,内容为 payload:
<script>alert(document.cookie)</script>或无需
<script>的变体:<img src=x onerror=alert(document.domain)> - 任何其他用户访问
/guestbook都会在浏览器中执行该脚本(持久化、影响所有访客)。 - 实际危害:窃取 Cookie(本应用未设
HttpOnly,document.cookie可读)、会话劫持、钓鱼弹窗、键盘记录、篡改页面。
修复建议
- 恢复模板自动转义:删除
{% autoescape false %}(默认即开启),或在输出处显式转义{{ comment|e }}。 - 输入侧白名单校验:只允许纯文本/受限标签,用
bleach等库清洗。 - 输出侧编码:统一对
& < > " ' /做 HTML 实体转义。 - 增加 CSP(内容安全策略) 头作为纵深防御,禁止内联脚本:
Content-Security-Policy: default-src 'self'; script-src 'self'
漏洞 #4:反射型 XSS 🟠 中
漏洞代码原因
routes.py 第 44 行读取用户可控参数:
search_query = request.args.get('q')
guestbook.html 第 45 行在自动转义关闭的情况下反射输出:
<h3 class="comment-header">Results for "{{ search_query }}"</h3>
根因: 与 #3 相同——autoescape false 导致 q 参数内容未经转义直接输出到 HTML。
利用方式
/guestbook?q=<script>alert(document.cookie)</script>
或 URL 编码形式。点击即触发,为反射型(非持久化),需诱导受害者点击恶意链接。
修复建议
- 输出转义:
{{ search_query|e }},或整体开启 autoescape。 - 对
q参数做长度与字符白名单限制。 - CSP 兜底。
漏洞 #5:硬编码 SECRET_KEY(CSRF 防护失效)🟠 中
漏洞代码原因
app.py 第 4 行:
app.config['SECRET_KEY'] = "suchSecret"
根因: SECRET_KEY 硬编码且公开在源码中(仓库可查、可预测)。
Flask-WTF 的 CSRF token 是基于该密钥签名的(guestbook.html:25 有 {{ form.csrf_token }}),
密钥泄露意味着攻击者可以自行构造/验证 CSRF token,使表单的 CSRF 防护形同虚设。
此外,若未来引入 session 登录态,该密钥泄露可直接导致会话伪造(任意用户会话劫持)。
利用方式
- 攻击者从源码获得
"suchSecret"。 - 用 Flask 的签名机制自行生成合法的 CSRF token(
itsdangerous的TimestampSigner),
绕过表单校验提交任意请求。 - 结合存储型 XSS(#3),攻击者可让受害者在不知情下提交任意操作。
修复建议
- 密钥从环境变量/配置文件读取,且每次部署随机生成:
import os app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY') or secrets.token_hex(32) - 密钥绝不进版本库,加入
.gitignore。 - 定期轮换密钥;泄露后立即轮换并吊销所有会话。
漏洞 #6:调试模式开启(Debug Mode)🟠 中
漏洞代码原因
app.py 第 8 行:
if __name__ == '__main__':
app.run(debug=True)
根因: debug=True 启用 Werkzeug 交互式调试器与 reloader,并开启详细堆栈回显。
利用方式 / 影响
- 信息泄露:触发 500 错误时,页面回显完整堆栈、源码片段、环境变量、文件路径。
- 潜在 RCE:Werkzeug 调试器提供交互式 Python 控制台(受 PIN 保护)。若服务器可达、PIN 被泄露/绕过,
可在服务端执行任意 Python 代码(读取文件、反弹 shell 等)。 - reloader 机制在生产中也不应开启。
修复建议
- 生产环境必须
debug=False,正式部署不要用内置开发服务器:if __name__ == '__main__': app.run(host='0.0.0.0', debug=False) - 用 gunicorn/uwsgi + nginx 部署,并统一自定义错误页,不泄露堆栈。
漏洞 #7:iframe 嵌入用户可控 URL 🟡 低(与 SSRF 关联)
漏洞代码原因
analyzer.html 第 15 行:
<iframe src="{{ req.url }}" frameborder="2" width="600" height="600"></iframe>
req.url 是攻击者通过 /analyzer?url= 传入并经服务端请求后的响应 URL,用户可控。
利用方式 / 影响
- 站内 iframe 可加载攻击者指定的任意 URL,用于站内嵌钓鱼页面、诱导点击。
- 该问题本质是 #2 SSRF 的回显面之一,不单独构成高危害,但与 SSRF 一并修复。
修复建议
- 不直接回显用户 URL;对响应内容做安全处理。
- 配合 CSP
frame-src 'self'限制 iframe 可加载来源。
漏洞 #8:缺少安全响应头 / 外部 CDN 依赖 🟡 低(加固建议)
现状
- 未设置
X-Frame-Options/CSP/X-Content-Type-Options/Strict-Transport-Security等安全头(配合 #3、#4 XSS 放大风险)。 guestbook.html:14从外部 CDN(stackpath.bootstrapcdn.com)加载 Bootstrap,存在供应链与可用性风险(外网不可达则样式失效)。- Cookie 未设置
Secure/HttpOnly/SameSite(本应用虽未主动种 cookie,但如引入会话需注意)。
修复建议
- 通过 Flask 中间件或反向代理(nginx)统一添加安全头。
- 静态资源本地化,或使用带
integrity的 SRI 与可信 CDN。 - 涉及 Cookie 时设置
Secure; HttpOnly; SameSite=Lax/Strict。
三、修复优先级建议
| 优先级 | 漏洞 | 理由 |
|---|---|---|
| P0 | #1 越权、#2 SSRF、#3 存储型 XSS | 影响大,攻击者无需前置条件即可利用 |
| P1 | #4 反射型 XSS、#5 硬编码密钥、#6 Debug 模式 | 常见入口,易被自动化工具扫描命中 |
| P2 | #7 iframe、#8 安全头/CDN | 纵深防御与加固 |
一句话总结: 该系统把"客户端输入一律可信 + 自动转义关闭 + 密钥写死 + 调试开启"这些反模式集齐了,
三处主漏洞(越权/SSRF/XSS)覆盖了 OWASP Top 10 中最典型的场景,作为靶场很有代表性;
若要加固,核心动作是:服务端会话鉴权、URL 白名单校验、恢复 autoescape、密钥外置、关闭 debug。
报告生成:代码静态审计 · 以 app.py / routes.py / db.py / forms.py 及各模板实际代码为准。