Web 安全初探

XSS(Cross Site Script,跨站脚本攻击)长期以来被列为客户端 Web 安全中的头号大敌,其本质就是一种「HTML 注入」,最简单的攻击场景就是:某网站有没有输入校验的输入框,而且不加处理地直接在页面上展示用户的输入内容,这样攻击者就可以输入一段 JavaScript 代码,浏览器展示用户输入内容的时候就会执行这段代码。

如下代码所示,我用 Flask 框架实现了几个接口,模拟了一下 XSS 攻击:假设这是一个博客网站,攻击者已经通过 XSS 漏洞在自己的博文中注入了一段 JavaScript 代码,为了简单起见这里直接把这段脚本存储在变量 xss_content 中,当一个正常用户登陆后要浏览这篇有问题的文章时,他的 Cookie 就被窃取了:为了简单起见这里只是通过一个弹窗把登陆用户的 Cookie 展示出来,攻击者完全可以通过注入的 JavaScript 代码把 Cookie 发送给自己,这种攻击类型叫做「Cookie 劫持」。

攻击者窃取用户的 Cookie 之后,可以直接构造请求访问需要登录态的接口,根本不需要拿到用户名和密码,如:

curl -X "POST" "http://wind4869.test:5000/modify" \
-H 'Cookie: session=eyJjc3JmX3Rva2VuIjoiMWU0OWMyYTJmMGQxOGIyMGQ3NDRhMmMyYWRkNGE3MmQxZmM4NjZlNSIsInVzZXJuYW1lIjoidGVzdCJ9.XOorbg.TBqn0RYXVyq39nkDX8xALP-5FI8' \
-H 'Content-Type: application/x-www-form-urlencoded; charset=utf-8' \
--data-urlencode "content=test"
# Finish Modification

防范「Cookie 劫持」相对简单,只需要在设置 Cookie 时带上 HttpOnly 的标识即可,这样客户端脚本就不能获取到被设置了标识的 Cookie 了,在 Flask 框架中默认会给 session 这个 Cookie 加上 HttpOnly ,下面的程序为了展示「Cookie 劫持」故意将 SESSION_COOKIE_HTTPONLY 配置设置成了 False。「Cookie 劫持」只是 XSS 能做的事情之一,即使不能窃取 Cookie,注入的 JavaScript 代码仍然能够拿到 HTTP 响应的其他内容,防止执行这样的代码才能彻底解决 XSS,一个简单的方案是对要返回的内容做 HTML 编码,例如:escape(xss_content) ,这样编码之后的内容就变成了纯文本:<script>alert(document.cookie)</script> ,不会再被浏览器执行。

# hello.py
# 127.0.0.1 wind4869.test
# export FLASK_APP=hello.py; flask run
from flask import Flask, session, redirect, url_for, escape, request, render_template
from flask_wtf.csrf import CSRFProtect

app = Flask(__name__)
csrf = CSRFProtect(app)

app.config['SESSION_COOKIE_HTTPONLY'] = False
app.config['SECRET_KEY'] = b'_5#y2L"F4Q8z\n\xec]/'

xss_content = '''
<script>alert(document.cookie)</script>
'''

@app.route('/')
def index():
if 'username' in session:
return xss_content
return "Please Login"

@csrf.exempt
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
session['username'] = request.form['username']
return redirect(url_for('index'))
return '''
<form method="post">
<input type=text name=username>
<input type=submit value=login>
</form>
'''

@app.route('/logout')
def logout():
session.pop('username', None)
return redirect(url_for('index'))

@csrf.exempt
@app.route('/modify', methods=['GET', 'POST'])
def csrf():
if 'username' not in session:
return redirect(url_for('index'))
if request.method == 'POST':
return 'Finish Modification'
return render_template('form.html')
<!-- templates/form.html -->
<form method="post">
<input type=text name=content />
<input type=submit value=modify />
<input type=hidden name=csrf_token value={{ csrf_token() }} />
</form>

CSRF(Cross Site Request Forgery,跨站请求伪造)也是一种常见的 Web 攻击方式,最常见的攻击场景是:用户已经登录了 A 网站,当用户访问 B 网站时,被诱导通过点击等方式触发了恶意脚本,发送了一个访问 A 网站的表单请求,对用户数据进行了修改。这个攻击之所以能够成立,是因为浏览器执行这个表单请求时会自动带上用户在 A 网站的 Cookie,直接访问下面这个 HTML 文件就能看到攻击的效果:

<!-- csrf.html -->
<!-- 127.0.0.1 wind4870.test -->
<!-- python -m SimpleHTTPServer -->
<form action="http://wind4869.test:5000/modify" id="csrf" method="post">
<input type="text" name="content" />
</form>
<script>
var form = document.getElementById("csrf");
form.elements[0].value = "test"
form.submit();
</script>
<!-- Finish Modification -->

Referer Check 是一种简单的「止血」方案,即通过检查 Referer 是否合法来判断用户是否被 CSRF 攻击,但是这种方案的缺陷在于,服务器并非什么时候都能取到 Referer,很多用户出于隐私保护的考虑,限制了 Referer 的发送。现在业界对 CSRF 的防御,一致的做法是使用 Anti CSRF Token。

CSRF 能够攻击成功的本质原因是:重要操作的所有参数都是可以被攻击者猜测到的。攻击者只有预测出 URL 的所有参数与参数值,才能成功地构造一个伪造的请求;反之,攻击者将无法攻击成功。一种通用的解决方案是:服务端生成一个随机 Token,同时放在表单和 Cookie 中,在提交请求时,服务器只需验证表单中的 Token 与用户 Cookie 中的 Token 是否一致,如果一致则认为是合法请求;如果不一致,或者有一个为空,则认为请求不合法,可能发生了 CSRF 攻击。

最开始的代码已经引入 flask_wtf 模块支持 CSRF 防御了,只是出于实验目的用 csrf.exempt 装饰器临时关闭了这个功能,移除这个注解重新启动 Flask 应用,再尝试进行 CSRF 攻击会返回 400,提示 The CSRF token is missing. 。Flask 框架把 csrf_token 放到了 session 这个 Cookie 中,上面的 form.html 也把 csrf_token 作为一个隐藏的 input 字段放到 form 中,所以正常的请求能够通过 Token 校验。

CSRF 的 Token 仅仅用于对抗 CSRF 攻击,当网站还同时存在 XSS 漏洞时,这个方案就会变得无效,因为 XSS 可以模拟客户端浏览器执行任意操作。在 XSS 攻击下,攻击者完全可以请求页面后,读出页面内容里的 Token 值,然后再构造出一个合法的请求,这个过程可以称为 XSRF,和 CSRF 以示区分。

这篇文章重度参考了吴翰清(道哥)的《白帽子讲 Web 安全》一书,如果想获取更深入的知识,可以直接阅读该书。

0%