![c02c282840554029bfa621beb367fd24.png](https://img-blog.csdnimg.cn/img_convert/c02c282840554029bfa621beb367fd24.png)
一、不安全的Web程序示例。
下面展示一个不安全的Web应用程序。
这个代码采用了Flask框架,但是不安全的原因不是因为使用了Flask。
#!/usr/bin/env python3
# A poorly-written and profoundly insecure payments application.
import bank
from flask import Flask, redirect, request, url_for
from jinja2 import Environment, PackageLoader
app = Flask(__name__)
get = Environment(loader=PackageLoader(__name__, 'templates')).get_template
@app.route('/login', methods=['GET', 'POST'])
def login():
username = request.form.get('username', '')
password = request.form.get('password', '')
if request.method == 'POST':
if (username, password) in [('brandon', 'atigdng'), ('sam', 'xyzzy')]:
response = redirect(url_for('index'))
response.set_cookie('username', username)
return response
return get('login.html').render(username=username)
@app.route('/logout')
def logout():
response = redirect(url_for('login'))
response.set_cookie('username', '')
return response
@app.route('/')
def index():
username = request.cookies.get('username')
if not username:
return redirect(url_for('login'))
payments = bank.get_payments_of(bank.open_database(), username)
return get('index.html').render(payments=payments, username=username,
flash_messages=request.args.getlist('flash'))
@app.route('/pay', methods=['GET', 'POST'])
def pay():
username = request.cookies.get('username')
if not username:
return redirect(url_for('login'))
account = request.form.get('account', '').strip()
dollars = request.form.get('dollars', '').strip()
memo = request.form.get('memo', '').strip()
complaint = None
if request.method == 'POST':
if account and dollars and dollars.isdigit() and memo:
db = bank.open_database()
bank.add_payment(db, username, account, dollars, memo)
db.commit()
return redirect(url_for('index', flash='Payment successful'))
complaint = ('Dollars must be an integer' if not dollars.isdigit()
else 'Please fill in all three fields')
return get('pay.html').render(complaint=complaint, account=account,
dollars=dollars, memo=memo)
if __name__ == '__main__':
app.debug = True
app.run()
上面的代码很危险,无法抵御现在Web上诸多向量的重要攻击。
先引入上述代码中出现的四个模板。
1.base.html页面的Jinja模板。
<html>
<head>
<title>{% block title %}{% endblock %}</title>
<link rel="stylesheet" type="text/css" href="/static/style.css">
</head>
<body>
<h1>{{ self.title() }}</h1>
{% block body %}{% endblock %}
</body>
</html>
这段代码中出现了value值,第一次显示该界面时,初始的value的值就会显示在可编辑文本框中。
2.login.html的Jinja2模板。
{% extends "base.html" %}
{% block title %}Please log in{% endblock %}
{% block body %}
<form method="post">
<label>User: <input name="username" value="{{ username }}"></label>
<label>Password: <input name="password" type="password"></label>
<button type="submit">Log in</button>
</form>
{% endblock %}
如果用户输入了错误的密码,用户重新输入时就可以不用再次输入他们的用户名。
3.index.html的Jinja2模板。
{% extends "base.html" %}
{% block title %}Welcome, {{ username }}{% endblock %}
{% block body %}
{% for message in flash_messages %}
<div class="flash_message">{{ message }}<a href="/">×</a></div>
{% endfor %}
<p>Your Payments</p>
<ul>
{% for p in payments %}
{% set prep = 'from' if (p.credit == username) else 'to' %}
{% set acct = p.debit if (p.credit == username) else p.credit %}
<li class="{{ prep }}">${{ p.dollars }} {{ prep }} <b>{{ acct }}</b>
for: <i>{{ p.memo }}</i></li>
{% endfor %}
</ul>
<a href="/pay">Make payment</a> | <a href="/logout">Log out</a>
{% endblock %}
很多情况下,用户会经常输入错误的表单信息。因此这段代码会检测是否接收到了complaint字符串。如果有的话,就显示在表单的顶部。这段代码冗余度比较高。如果表单信息有误且需要重新显示的话。就需要有三个表单字段,并且要使用用户试图提交时来实现填充这三个字段。
4.pay.html中的Jiaja2模板。
{% extends "base.html" %}
{% block title %}Make a Payment{% endblock %}
{% block body %}
<form method="post" action="/pay">
{% if complaint %}<span class="complaint">{{ complaint }}</span>{% endif %}
<label>To account: <input name="account" value="{{ account }}"></label>
<label>Dollars: <input name="dollars" value="{{ dollars }}"></label>
<label>Memo: <input name="memo" value="{{ memo }}"></label>
<button type="submit">Send money</button> | <a href="/">Cancel</a>
</form>
{% endblock %}
在设计网站时,最好在每个提交按钮旁边都提供取消功能。但是取消功能的元素要比默认的表单提交按钮小很多。这样子用户操作失误将会降到最低。
二、下面说一下表单和HTTP方法。
HTML表单的默认action是GET。它可以只包含一个文本框。
进行GET的表单会把输入的字段直接放入URL中,然后将其作为HTTP请求的路径。
这将意味着GET的参数将成为浏览历史的一部分。所以绝对不能用GET来传输密码或者证书这样的敏感信息。
另外一种表单(POST,PUT和DELETE的表单)在URL中绝对不会包含任何表单信息。表单信息也不会出现在HTTP请求的路径中。
浏览器会把所有数据都放入请求消息体中,而请求的路径本身是没有变化的。
Web浏览器知道POST是一个可能会造成状态变化的操作。所以在处理POST请求时候都会很小心。如果用户在浏览一个有POST请求返回的页面时重新加载网页,那么浏览器将会弹出一个对话框。一般都会输出一个警告。
重复提交表单的后果是非常严重的。
为了防治重新加载,或者在前进、后退的时候不断收到浏览器的对话框。有两种技术可以供网站采用。
- 使用JavaScript或者HTML5表单的输入限制来尝试实现防止用户提交含有非法值的表单。在符合要求之前禁用提交按钮。或者使用JavaScript在不需要重新加载页面的情况下提交整个表单并获取响应。
- 当表单已经正确提交并且成功执行了POST请求的动作之后,Web应用程序不应该返回已经完成的200 OK界面。而是立即进行一个GET请求,立刻转到GET请求要访问的界面。
三、安全的cookie和不安全的cookie。
上述的代码中试图保护用户的隐私。必须登录成功后才能通过“/”的GET请求查看账单列表。如果想通过POST请求来进行支付。用户必须成功登陆。
但是在登陆之后,服务器的相应信息会包含一个名称为username的cookie,并且初始值已经被设置为badguy。显然,后续只要包含这个cookie,那么网站就会认为发送这些请求的用户已经输入了正确的用户名和密码。
这个时候很不安全,我们可以伪造cookie。只需要账单系统中的另外一个已经登陆的用户,我们就可以用伪造请求。
因此要保证无法被伪造,有3种安全的方法。
- 保留可读性,利用数字签名对cookie签名。这样子他们可以伪造cookie,但是无法伪造签名。所以网站不会信任他们重新构造的cookie。
- 对cookie进行完全加密。用户无法读懂,可能计算机也无法解析。
- 可以使用一个纯随机的字符串作为cookie。将这些字符串存在自己的数据库中。每个用户对应一个随机的字符串。如果是用户的HTTP可能被转发到多台不同的服务器,那么所有的服务器都要能够访问这一个持久化的session存储。有一些存储到核心数据库中。有一些程序使用Redis实例或者其他存储器较短的方式,以防止核心数据库的查询负载过高。
四、非持久型跨站脚本。
恶意用户无法窃取或者伪造cookie,也就无法浏览器或者Python程序伪装成另外一个用户。
如果他们可以控制另外一个已经登录用户的浏览器,就不需要查看cookie,而直接通过浏览器的请求,请求中就会自动包含正确的cookie。
第一种是非持久型的跨站脚本(XSS)。攻击者将会自己的脚本插入网站,网站把这些脚本当作自己的脚本使用。比如攻击者想要向他们的账户支付110美元。可以写如下的代码:
<script>
var x = new XMLHttpRequest();
x.open('POST', 'http://localhost:5000/pay');
x.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
x.send('account=hacker&dollars=110&memo=Theft');
</script>
攻击者将这段包含JavaScript脚本的HTML注入页面。
攻击者只需要伪造一个入口,诱使用户看到并点击设计好的URL。
为了防止被用户发现,经常会分享一个短链接,只有用户点击之后,短连接才会扩展成为包含跨站脚本的原始完整链接。
五、持久型跨站脚本。
如果攻击者无法再一个又长又丑的URL中设置陷阱。那就必须用其他的方法注入JavaScript。扫视了一遍主页,他们可能会在显示账单的Memo字段中做手脚。
当然,要在页面上显示精心设计的Memo要比直接将Memo嵌入URL中会复杂一些。而且攻击者是可以直接将URL匿名提供给用户的。要在页面上显示Memo,就必须使用虚假的个人信息注册网站。或者利用另一个用户的账户向受害者进行一个支付,并且在Memo字段中包含<script>元素以及上面的那一段JavaScript脚本。
每次访问首页时,都会因为运行脚本而支出一笔账单。
持久型跨站脚本攻击,只要受害者访问了网站,JavaScript脚本就会不断的隐式运行。直到服务器上的数据都被清空。
上述代码,之所以无法抵御这一类型的攻击。是因为Jinja2的开发者没有理解Jinja2的使用方法。它并不会自动进行任何转义。
六、跨站请求伪造。
当我们对所有内容进行转义,不用再担心XSS攻击。
但是他们可以从另外一个完全不同的网站提交表单。
这里不再展开介绍。感兴趣的同学可以查一下相关的资料。
由于用户的浏览器启用了JavaScript,攻击者可以直接把代码中的<script>元素插入用户要载入的页面、论坛帖子或者评论中。然后坐等受害者的钱流入他们的账户。
如何解决这个问题呢?
我们可以通过增加构造以及提交表单的难度。比如表单中增加一个额外的字段,其中包含只对表单的合法用户或者合法用户的浏览器私钥。这样攻击者也就无法为造出服务器信任的POST请求。
七、改进的应用程序。
下面是改进后的程序。
#!/usr/bin/env python3
# A payments application with basic security improvements added.
import bank, uuid
from flask import (Flask, abort, flash, get_flashed_messages,
redirect, render_template, request, session, url_for)
app = Flask(__name__)
app.secret_key = 'saiGeij8AiS2ahleahMo5dahveixuV3J'
@app.route('/login', methods=['GET', 'POST'])
def login():
username = request.form.get('username', '')
password = request.form.get('password', '')
if request.method == 'POST':
if (username, password) in [('brandon', 'atigdng'), ('sam', 'xyzzy')]:
session['username'] = username
session['csrf_token'] = uuid.uuid4().hex
return redirect(url_for('index'))
return render_template('login.html', username=username)
@app.route('/logout')
def logout():
session.pop('username', None)
return redirect(url_for('login'))
@app.route('/')
def index():
username = session.get('username')
if not username:
return redirect(url_for('login'))
payments = bank.get_payments_of(bank.open_database(), username)
return render_template('index.html', payments=payments, username=username,
flash_messages=get_flashed_messages())
@app.route('/pay', methods=['GET', 'POST'])
def pay():
username = session.get('username')
if not username:
return redirect(url_for('login'))
account = request.form.get('account', '').strip()
dollars = request.form.get('dollars', '').strip()
memo = request.form.get('memo', '').strip()
complaint = None
if request.method == 'POST':
if request.form.get('csrf_token') != session['csrf_token']:
abort(403)
if account and dollars and dollars.isdigit() and memo:
db = bank.open_database()
bank.add_payment(db, username, account, dollars, memo)
db.commit()
flash('Payment successful')
return redirect(url_for('index'))
complaint = ('Dollars must be an integer' if not dollars.isdigit()
else 'Please fill in all three fields')
return render_template('pay2.html', complaint=complaint, account=account,
dollars=dollars, memo=memo,
csrf_token=session['csrf_token'])
if __name__ == '__main__':
app.debug = True
app.run()
一个程序想要完全没有安全漏洞是困难的。只能尽可能的避免。
文章到了这里就结束了。谢谢大家关注。
这是我新的一年的第一篇文章。
希望大家在新的一年里。万事顺意。