DVFA-代码审计报告

30次阅读
没有评论

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)或数据库核验真实用户身份与权限。

利用方式

  1. 直接访问 GET /admin,默认返回 403。
  2. 使用 Burp Suite / 浏览器插件 / 开发者工具,在请求头中注入 Cookie:
    Cookie: admin=True
    

    或执行 JS:document.cookie = "admin=True"。

  3. 重新访问 /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 原样渲染给所有访客。

利用方式

  1. 在留言板提交一条评论,内容为 payload:
    <script>alert(document.cookie)</script>
    

    或无需 <script> 的变体:

    <img src=x onerror=alert(document.domain)>
    
  2. 任何其他用户访问 /guestbook 都会在浏览器中执行该脚本(持久化、影响所有访客)。
  3. 实际危害:窃取 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 登录态,该密钥泄露可直接导致会话伪造(任意用户会话劫持)。

利用方式

  1. 攻击者从源码获得 "suchSecret"。
  2. 用 Flask 的签名机制自行生成合法的 CSRF token(itsdangerous 的 TimestampSigner),
    绕过表单校验提交任意请求。
  3. 结合存储型 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 及各模板实际代码为准。

正文完
 0