本节内容将通过构建一个带有各种漏洞为Web应用,演示Web安全中较为经典的案例:跨站脚本攻击。
在此之前你需要了解这样一些内容:
- 了解基本的HTTP知识(GET、POST请求及其参数)
- 了解基本的Web前端知识(HTML、JavaScript)
- 了解基本的数据库知识(SQL)
本节内容涉及的技术环境:
- Python 2.7
- SQLite3
- Flask
相关文档自行百度,关键字即为上述内容。为方便读者自行研究,文末会给出本例中的一份完整的参考代码。
环境准备
安装所需软件
pip install flask
建议使用PyCharm作为编程环境,便于自动import
一些需要的库。
问题背景
本文通过先构建一个最小的“社交应用”,并刻意在应用中留下一些漏洞进行演示和分析,以此向读者阐明此类安全问题的原理、引发的原因。
这个社交应用的基本功能如下:
- 用户登录
- 用户在time line中发帖
- 用户删除time line中的某一项
即类似QQ空间发说说的功能,每个用户都能发表说说,用户也能看到其他人发表的说说。
应用设计
数据库
整个应用需要有两个数据库表,一个为user
表,存放用户信息,一个为time_line
表,存放用户发表的内容。
相应的DDL如下:
CREATE TABLE IF NOT EXISTS user(
id INTEGER PRIMARY KEY AUTOINCREMENT,
username VARCHAR(32),
password VARCHAR(32)
);
CREATE TABLE IF NOT EXISTS time_line(
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
content TEXT,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`)
);
time_line
表中通过外键user_id
与user
表中的用户建立关联,表明某个time_line
和某个用户之间的所述关系。
controllers
这里暂且叫controller
吧。整个应用对外暴露了下面这些URL:
/
应用首页:若用户已登录,则展示所有用户在time line
下留下的记录;若用户未登录,则跳转到登录界面
/login
登录界面:用户在此进行登录
/create_time_line
创建一个time line
,接受浏览器发来的POST
请求,创建一个time line
并存入数据库。
/delete/time_line/
删除一个time line
,用户访问此URL并附上time line
的id
后即可删除对应的time line
,URL中的
即为time line
的id
。
功能实现
数据库创建
在开发功能之前,先准备一些数据,便于测试。准备数据工作有两个内容:
- 建立数据库表
- 初始化测试数据
先准备一个文件:app.py
,开始写代码:
- 先准备连接数据库
DATABASE_PATH = os.path.join(os.path.dirname(__file__), 'database.db')
def connect_db():
return sqlite3.connect(DATABASE_PATH)
- 创建数据库表
def create_tables():
conn = connect_db()
cur = conn.cursor()
cur.execute('''
CREATE TABLE IF NOT EXISTS user(
id INTEGER PRIMARY KEY AUTOINCREMENT,
username VARCHAR(32),
password VARCHAR(32)
)''')
cur.execute('''
CREATE TABLE IF NOT EXISTS time_line(
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
content TEXT,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`)
)''')
conn.commit()
conn.close()
- 插入一些简单的测试数据
def init_data():
users = [
('user1', '123456'),
('user2', '123456')
]
lines = [
(1, 'Hello'),
(1, 'World'),
(2, 'Im 2'),
(2, 'Hello 2')
]
conn = connect_db()
cur = conn.cursor()
cur.executemany('INSERT INTO `user` VALUES(NULL,?,?)', users)
cur.executemany('INSERT INTO `time_line` VALUES(NULL,?,?)', lines)
conn.commit()
conn.close()
- 把这两个功能组合起来,一会儿方便用
def init():
create_tables()
init_data()
到此,app.py
中包含了初始化数据库的功能。
需要初始化数据的时候可以这么做,在app.py
所在的文件夹下执行python
,进入shell
调用init
函数:
$ python
>>> from app import *
>>> init()
>>> exit()
这样,数据就初始化好了。测试数据集中有两个用户,每个用户分别都发了两条动态。
功能封装
在开始编写各个controller
之前,先将整个应用的基础功能进行一次封装,方便稍后使用:
- 根据用户名、密码查询用户(若没有找到匹配的用户则返回
None
)
def get_user_from_username_and_password(username, password):
conn = connect_db()
cur = conn.cursor()
cur.execute('SELECT id, username FROM `user` WHERE username=\'%s\' AND password=\'%s\'' % (username, password))
row = cur.fetchone()
conn.commit()
conn.close()
return {'id': row[0], 'username': row[1]} if row is not None else None
- 根据用户的
id
查询对应的用户
def get_user_from_id(uid):
conn = connect_db()
cur = conn.cursor()
cur.execute('SELECT id, username FROM `user` WHERE id=%d' % uid)
row = cur.fetchone()
conn.commit()
conn.close()
return {'id': row[0], 'username': row[1]}
- 根据用户的
id
和time line
的内容创建一条动态
def create_time_line(uid, content):
conn = connect_db()
cur = conn.cursor()
cur.execute('INSERT INTO `time_line` VALUES (NULL, %d, \'%s\')' % (uid, content))
row = cur.fetchone()
conn.commit()
conn.close()
return row
- 获取所有用户的动态信息(返回的数据格式为字典数组)
def get_time_lines():
conn = connect_db()
cur = conn.cursor()
cur.execute('SELECT id, user_id, content FROM `time_line` ORDER BY id DESC')
rows = cur.fetchall()
conn.commit()
conn.close()
return map(lambda row: {'id': row[0], 'user_id': row[1], 'content': row[2]}, rows)
- 根据用户的
id
和动态的id
删除一个动态
def user_delete_time_line_of_id(uid, tid):
conn = connect_db()
cur = conn.cursor()
cur.execute('DELETE FROM `time_line` WHERE user_id=%s AND id=%s' % (uid, tid))
conn.commit()
conn.close()
有了这样一些基本功能的封装之后,后面需要使用的时候就可以直接在controller
函数里调用了。
视图
整个Web应用有两个界面:登录界面、动态列表界面。为了简化程序,这里的这两个界面都是直接通过返回符合html
语法的字符串直接产生,而不再去设置单独的html
作为模板。
两个视图如下:
- 登录界面
def render_login_page():
return '''
'''
- 动态列表界面
def render_home_page(uid):
user = get_user_from_id(uid)
time_lines = get_time_lines()
template = Template('''
I am: {{ user['username'] }}
{% for line in time_lines %}
-
{{ line['content'] }}
{% if line['user_id'] == user['id'] %}
Delete
{% endif %}
{% endfor %}
''')
return template.render(user=user, time_lines=time_lines)
这两个函数目前仅仅负责根据传入的参数,查询数据库并通过字符串模板渲染成html
字符串,然后返回渲染后的html
。
render_home_page
用于渲染整个应用的首页,也就是所有用户的动态列表,当某个动态的所有者为当前用户的时候,还会在动态中渲染出一个对应的删除链接,方便用户删除动态。
controllers
在对应的controller
中编写应用的业务逻辑,前面先拉出一长串代码,就是为了进行足够的封装,让整个应用的controller
部分足够简洁清晰,整个应用的逻辑一目了然。
以下便是整个应用的主体框架:
@app.route('/')
def index():
if 'uid' in session:
return render_home_page(session['uid'])
return redirect('/login')
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return render_login_page()
elif request.method == 'POST':
username = request.form['username']
password = request.form['password']
user = get_user_from_username_and_password(username, password)
if user is not None:
session['uid'] = user['id']
return redirect('/')
else:
return redirect('/login')
@app.route('/create_time_line', methods=['POST'])
def time_line():
if 'uid' in session:
uid = session['uid']
create_time_line(uid, request.form['content'])
return redirect('/')
@app.route('/delete/time_line/')
def delete_time_line(tid):
if 'uid' in session:
user_delete_time_line_of_id(session['uid'], tid)
return redirect('/')
@app.route('/logout')
def logout():
if 'uid' in session:
session.pop('uid')
return redirect('/login')
if __name__ == '__main__':
app.run(debug=True)
这部分代码就比较简单了。稍微说明一下,用户登录成功后,会在session
中保存用户的id
,以此标识当前登录的用户。
应用首页会判断用户是否登录,若用户未登录,则会跳转到登录界面要求用户登录。若用户已登录,则渲染首页,首页中的内容为所有用户发布的动态。
好了,整个应用到这里基本就搭建完了。
测试
启动程序:
$ python app.py
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger pin code: 233-701-647
如果你运行python app.py
的时候看到上述结果,说明整个应用基本构建好了。接下来可以开始尝试了。
按照提示,访问http://127.0.0.1:5000/
,按照预期,浏览器会自动跳转到登录界面http://127.0.0.1:5000/login
,还记得前面创建的测试数据吗?随便选择一个用户登录进去,比如这里使用user1:123456
进行登录。
不出意外的话,登录之后便能看到测试数据中的内容了。在界面上方的文本框中输入一些内容,点击Submit
后即可创建新的动态(相当于在空间中发了一条新的说说)。
你还可以点击自己发的动态下方的Delete
删除对应的动态。
到这里,应用环境的搭建才算完工。
来一发
现在要开始搞事情了,验证下环境有没有设置好。在创建动态的文本框中输入如下内容作为动态:
XSSed
之后提交,创建动态,看看页面刷新后会发生什么。
如果你发现了诡异的现象,说明环境搭建成功了。
跨站脚本攻击
上述实例演示了一个简单的跨站脚本攻击,其实也就是当用户输入的内容会渲染到HTML
页面中的时候,就有可能存在这样的问题。
即我在文本框中输入一段含有script
标签的html
代码,之后再次访问页面时,页面中包含了刚才构造出的特殊内容,这段内容会被浏览器傻乎乎地当作脚本也执行了。
当然,上述示例只是弹出了一个框而已,可能感觉仅仅是影响使用,构不成大威胁。那可难说了,笔者后续会引出一些危害更大的代码,到时候就可以感受到这个问题时多么的可怕了。比如这样恶搞一下:
在小白用户面前,这个应用就已经瘫掉了没法用了,这对一个互联网产品来说危害也相当大。
Summary
后续内容将会给出更多更恶劣的问题,以及给出一些对应的解决方法。
在此就可以开始Web安全的了解了,并非说必须得是安全从业者才需要了解安全相关的内容,应用开发者也同样需要学习,保证开发过程中不会犯下低级错误,最终可能会导致产品发布后受到比较恶劣的影响。
上述示例的代码可以在这里找到:
https://gist.github.com/hackeris/fa2bfd20e6bec08c8d5240efe87d4687
文中若有疏漏或不当之处,欢迎指正。