无论是一个简单的博客,还是大型的社交网站,Web安全都应该放在首位。Web安全问题涉及广泛,在这里介绍其中常见的几种攻击 (attack)和其他常见的漏洞(vulnerability。
对于Web程序的安全问题,一个首要的原则是:永远不要相信你的用户。大部分Web安全问题都是因为没有对用户输入的内容进行“消毒”造成的。
1.注入攻击
在OWASP(Open Web Application Security Project,开放式Web程序安全项目)发布的最危险的Web程序安全风险Top 10中,无论是最新的2017年的排名,2013年的排名还是最早的2010年,注入攻击 (Injection)都位列第一。注入攻击包括系统命令(OS Command)注入、SQL(Structured Query Language,结构化查询语言)注入(SQL Injection)、NoSQL注入、ORM(Object Relational Mapper,对象关系
映射)注入等。我们这里重点介绍的是SQL注入。
(1)攻击原理
在编写SQL语句时,如果直接将用户传入的数据作为参数使用字符串拼接的方式插入到SQL查询中,那么攻击者可以通过注入其他语句来 执行攻击操作,这些攻击操作包括可以通过SQL语句做的任何事:获取敏感数据、修改数据、删除数据库表……
(2)攻击示例
假设我们的程序是一个学生信息查询程序,其中的某个视图函数接收用户输入的密码,返回根据密码查询对应的数据。我们的数据库由一 个db对象表示,SQL语句通过execute()方法执行:
@app.route('/students')
def bobby_table():
password = request.args.get('password')
cur = db.execute("SELECT * FROM students WHERE password='%s';" % password)
results = cur.fetchall()
return results
注:在实际应用中,敏感数据需要通过表单提交的POST请求接收,这 里为了便于演示,我们通过查询参数接收。
我们通过查询字符串获取用户输入的查询参数,并且不经过任何处理就使用字符串格式化的方法拼接到SQL语句中。在这种情况下,如果 攻击者输入的password参数值为“'or 1=1--”, 那么最终视图函数中 被执行的SQL语句将变为:
SELECT * FROM students WHERE password='' or 1=1 --;'
这时会把students表中的所有记录全部查询并返回,也就意味着所有的记录都被攻击者窃取了。更可怕的是,如果攻击者将password参数 的值设为“';drop table users;--”,那么查询语句就会变成:
SELECT * FROM students WHERE password=''; drop table students; --;
执行这个语句会把students表中的所有记录全部删除掉。(“--”用来注释后面的语句)
(3)主要防范方法
1)使用ORM可以一定程度上避免SQL注入问题。
2)验证输入类型。比如某个视图函数接收整型id来查询,那么就 在URL规则中限制URL变量为整型。
3)参数化查询。在构造SQL语句时避免使用拼接字符串或字符串
格式化(使用百分号或format()方法)的方式来构建SQL语句。而要使用各类接口库提供的参数化查询方法。
db.execute('SELECT * FROM students WHERE password=?, password)
4)转义特殊字符,比如引号、分号和横线等。
2 XSS攻击
XSS(Cross-Site Scripting,跨站脚本)攻击
(1)攻击原理
XSS是注入攻击的一种,攻击者通过将代码注入被攻击者的网站 中,用户一旦访问网页便会执行被注入的恶意脚本。XSS攻击主要分为 反射型XSS攻击(Reflected XSS Attack)和存储型XSS攻击(Stored XSS Attack)两类。
反射型XSS又称为非持久型XSS(Non-Persistent XSS)。当某个站点存在XSS漏洞时,这种攻击会通过URL注入攻击脚本,只有当用户访问这个URL时才会执行攻击脚本。我们在本章前面介绍查询字符串和 cookie时引入的示例就包含反射型XSS漏洞,如下所示:
@app.route('/hello')
def hello():
name = request.args.get('name')
response = '<h1>Hello, %s!</h1>' % name
这个视图函数接收用户通过查询字符串传入的数据,未做任何处理 就把它直接插入到返回的响应主体中,返回给客户端。如果某个用户输入了一段JavaScript代码作为查询参数name的值,如下所示:
name=<script>alert('Bingo!');</script>
当客户端接收到响应后,浏览器解析这行代码就会打开一个弹窗,你觉得一个小弹窗不会造成什么危害?那你就完全错了,能够执行 alert()函数就意味着通过这种方式可以执行任意JavaScript代码。即攻击者通过JavaScript几乎能够做任何事情:窃取用户的cookie和其他敏感 数据,重定向到钓鱼网站,发送其他请求,执行诸如转账、发布广告信 息、在社交网站关注某个用户等。即使不插入JavaScript代码,通过HTML和CSS(CSS注入)也可以 影响页面正常的输出,篡改页面样式,插入图片等。
存储型XSS也被称为持久型XSS(persistent XSS),这种类型的 XSS攻击更常见,危害也更大。它和反射型XSS类似,不过会把攻击代码储存到数据库中,任何用户访问包含攻击代码的页面都会被殃及。比如,某个网站通过表单接收用户的留言,如果服务器接收数据后未经处 理就存储到数据库中,那么用户可以在留言中插入任意JavaScript代码。 比如,攻击者在留言中加入一行重定向代码:
<script>window.location.href="http://attacker.com";</script>
其他任意用户一旦访问留言板页面,就会执行其中的JavaScript脚本。就会被重定向到攻击者写入的站点。
(3)主要防范措施
a.HTML转义
防范XSS攻击最主要的方法是对用户输入的内容进行HTML转义, 转义后可以确保用户输入的内容在浏览器中作为文本显示,而不是作为代码解析。这里的转义和Python中的概念相同,即消除代码执行时的歧义,也就是把变量标记的内容标记为文本,而不是HTML代码。具体来说,这会把变量中与HTML相关的符号转换为安全字符,以避免变量中包含影响页面输出的HTML标签或恶意的JavaScript代码。
比如,我们可以使用Jinja2提供的escape()函数对用户传入的数据 进行转义:
from jinja2 import escape
@app.route('/hello')
def hello():
name = request.args.get('name')
response = '<h1>Hello, %s!</h1>' % escape(name)
b.验证用户输入
XSS攻击可以在任何用户可定制内容的地方进行,例如图片引用、自定义链接。仅仅转义HTML中的特殊字符并不能完全规避XSS攻击,因为在某些HTML属性中,使用普通的字符也可以插入JavaScript代码。 除了转义用户输入外,我们还需要对用户的输入数据进行类型验证。在 所有接收用户输入的地方做好验证工作。
以某个程序的用户资料页面为例,我们来演示一下转义无法完全避免的XSS攻击。。程序允许用户输入个人资料中的个人网站地址,通过下面的方式显示在资料页面中:
<a href="{{ url }}">Website</a>
其中{{url}}部分表示会被替换为用户输入的url变量值。如果不对 URL进行验证,那么用户就可以写入JavaScript代码,比如“javascript: alert('Bingo!');”。因为这个值并不包含会被转义的<和>。最终页面 上的链接代码会变为:
<a href="javascript:alert('Bingo!');">Website</a>
类似的,{{url}}部分表示会被替换为用户输入的url变量值。如果不对输入的URL进行验证,那么用户可以将url设为“123"onerror="alert('Bingo!')”,最终的<img>标签就会变为:
<img src="123" onerror="alert('Bingo!')">
在这里因为src中传入了一个错误的URL,浏览器便会执行onerror属 性中设置的JavaScript代码。
所以需要对用户输入进行验证
3.CSRF攻击
CSRF(Cross Site Request Forgery,跨站请求伪造)是一种近年来 才逐渐被大众了解的网络攻击方式,又被称为One-Click Attack或Session Riding。
(1)攻击原理
CSRF攻击的大致方式如下:某用户登录了A网站,认证信息保存在 cookie中。当用户访问攻击者创建的B网站时,攻击者通过在B网站发送 一个伪造的请求提交到A网站服务器上,让A网站服务器误以为请求来自于自己的网站,于是执行相应的操作,该用户的信息便遭到了篡改。总结起来就是,攻击者利用用户在浏览器中保存的认证信息,向对应的站点发送伪造请求。在前面学习cookie时,我们介绍过用户认证通过保存在cookie中的数据实现。在发送请求时,只要浏览器中保存了对应的 cookie,服务器端就会认为用户已经处于登录状态,而攻击者正是利用了这一机制。
(2)攻击示例
假设我们网站是一个社交网站简称网站A;攻击者的网站可以是任意类型的网站,简称网站B。在我们的网站中,删除账户的操作通过GET请求执行,由使用下面的delete_account视图处理:
@app.route('/account/delete')
def delete_account():
if not current_user.authenticated:
abort(401)
current_user.delete()
return 'Deleted!'
当用户登录后,只要访问http://A.com/account/delete就会删 账户。那么在攻击者的网站上,只需要创建一个显示图片的img标签, 其中的src属性加入删除账户的URL:
<img src="http://A.com/account/delete">
当用户访问B网站时,浏览器在解析网页时会自动向img标签的src 属性中的地址发起请求。此时你在A网站的登录信息保存在cookie中, 因此,仅仅是访问B网站的页面就会让你的账户被删除掉。
当然,现实中很少有网站会使用GET请求来执行包含数据更改的敏感操作,这里只是一个示例。现在,假设我们吸取了教训,改用POST 请求提交删除账户的请求。尽管如此,攻击者只需要在B网站中内嵌一 个隐藏表单,然后设置在页面加载后执行提交表单的JavaScript函数,攻 击仍然会在用户访问B网站时发起。
(3)主要防范措施
a.正确使用HTTP方法
防范CSRF的基础就是正确使用HTTP方法。在前面我们介绍过 HTTP中的常用方法。在普通的Web程序中,一般只会使用到GET和 POST方法。而且,目前在HTML中仅支持GET和POST方法(借助 AJAX则可以使用其他方法)。在使用HTTP方法时,通常应该遵循下面 的原则:
-GET方法属于安全方法,不会改变资源状态,仅用于获取资源, 因此又被称为幂等方法(idempotent method)。页面中所有可以通过链接发起的请求都属于GET请求。
-POST方法用于创建、修改和删除资源。在HTML中使用form标签 创建表单并设置提交方法为POST,在提交时会创建POST请求。
b.CSRF令牌校验
当处理非GET请求时,要想避免CSRF攻击,关键在于判断请求是否来自自己的网站。在前面我们曾经介绍过使用HTTP referer获取请求来源,理论上说,通过referer可以判断源站点从而避免CSRF攻击,但因 为referer很容易被修改和伪造,所以不能作为主要的防御措施。
除了在表单中加入验证码外,一般的做法是通过在客户端页面中加入伪随机数来防御CSRF攻击,这个伪随机数通常被称为CSRF令牌 (token)。
在HTML中,POST方法的请求通过表单创建。我们把在服务器端创建的伪随机数(CSRF令牌)添加到表单中的隐藏字段里和session变量(即签名cookie)中,当用户提交表单时,这个令牌会和表单数据一起提交。在服务器端处理POST请求时,我们会对表单中的令牌值进 验证,如果表单中的令牌值和session中的令牌值相同,那么就说明请求发自自己的网站。因为CSRF令牌在用户向包含表单的页面发起GET请求时创建,并且在一定时间内过期,一般情况下攻击者无法获取到这个令牌值,所以我们可以有效地区分出请求的来源是否安全。
除了这几个攻击方式外,我们还有很多安全问题要注意。比如文件上传漏洞、敏感数据存储、用户认证(authentication)与权限管理等。
这些内容我们将在后面的陆续介绍。