摘自《Flask Web开发实战:入门、进阶与原理解析(李辉著 )》

对于Web程序的安全问题,一个首要的原则是:永远不要相信你的用户。大部分Web安全问题都是因为没有对用户输入的内容进行「消毒」造成的。

1.注入攻击

注入攻击包括系统命令(OS Command)注入、SQL注入(SQL Injection)、NoSQL注入、ORM注入等。

下面重点介绍SQL注入。

(1)攻击原理

在编写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

我们通过查询字元串获取用户输入的查询参数,并且不经过任何处理就使用字元串格式化的方法拼接到SQL语句中。在这种情况下,如果攻击者输入的password参数值为「or 1=1--」,即http://example.com/students?password=or 1=1--,那么最终视图函数中被执行的SQL语句将变为:

SELECT * FROM students WHERE password= or 1=1 --;

在SQL中,「;」用来结束一行语句;「--」用来注释后面的语句。这时会把students表中的所有记录全部查询并返回。

(3)主要防范方法

1)使用ORM可以一定程度上避免SQL注入问题。

2)验证输入类型。比如某个视图函数接收整型id来查询,那么就在URL规则中限制URL变数为整型。

3)参数化查询。在构造SQL语句时避免使用拼接字元串或字元串格式化(使用百分号或format()方法)的方式来构建SQL语句。而要使用各类介面库提供的参数化查询方法,以内置的sqlite3库为例:

db.execute(SELECT * FROM students WHERE password=?, password)

4)转义特殊字元,比如引号、分号和横线等。使用参数化查询时,各种介面库会为我们做转义工作。

2.XSS(Cross-Site Scripting)攻击

(1)攻击原理

XSS是注入攻击的一种,攻击者通过将代码注入被攻击者的网站中,用户一旦访问网页便会执行被注入的恶意脚本。XSS攻击主要分为反射型XSS攻击(Reflected XSS Attack)和存储型XSS攻击(Stored XSS Attack)两类。

(2)攻击示例

反射型XSS又称为非持久型XSS(Non-Persistent XSS)。当某个站点存在XSS漏洞时,这种攻击会通过URL注入攻击脚本,只有当用户访问这个URL时才会执行攻击脚本。

@app.route(/hello)
def hello():
name = request.args.get(name)
response = <h1>Hello, %s!</h1> % name

这个视图函数接收用户通过查询字元串传入的数据,未做任何处理就把它直接插入到返回的响应主体中,返回给客户端。如果某个用户输入了一段JavaScript代码作为查询参数name的值,如下所示:

http://example.com/hello?name=<script>alert(Bingo!);</script>

客户端接收的响应将变为下面的代码:

<h1>Hello, <script>alert(Bingo!);</script>!</h1>

当客户端接收到响应后,浏览器解析这行代码就会打开一个弹窗。

攻击者通过JavaScript几乎能够做任何事情:窃取用户的cookie和其他敏感数据,重定向到钓鱼网站,发送其他请求,执行诸如转账、发布广告信息、在社交网站关注某个用户等。

即使不插入JavaScript代码,通过HTML和CSS(CSS注入)也可以影响页面正常的输出,篡改页面样式,插入图片等。

XSS攻击流程:网站A存在XSS漏洞,攻击者将包含攻击代码的网站A的链接发送给用户Foo,当Foo访问这个链接,伺服器就会产生带有攻击代码的响应给用户Foo,用户Foo的浏览器就会执行攻击代码。

存储型XSS也被称为持久型XSS(persistent XSS)。它和反射型XSS类似,不过会把攻击代码储存到资料库中,任何用户访问包含攻击代码的页面都会被殃及。

比如,某个网站通过表单接收用户的留言,如果伺服器接收数据后未经处理就存储到资料库中,那么用户可以在留言中插入任意JavaScript代码。

比如,攻击者在留言中加入一行重定向代码:

<script>window.location.href="http://attacker.com";</script>

其他任意用户一旦访问留言板页面,就会执行其中的JavaScript脚本。

(3)主要防范措施

a.HTML转义

防范XSS攻击最主要的方法是对用户输入的内容进行HTML转义,转义后可以确保用户输入的内容在浏览器中作为文本显示,而不是作为代码解析。就是把变数标记的内容标记为文本,而不是HTML代码。

我们可以使用Jinja2提供的escape()函数对用户传入的数据进行转义:

from jinja2 import escape
@app.route(/hello)
def hello():
name = request.args.get(name)
response = <h1>Hello, %s!</h1> % escape(name)

前面的示例中,用户输入的JavaScript代码将被转义为:

&lt;script&gt;alert(&#34;Bingo!&#34;)&lt;/sript&gt;

转义后,文本中的特殊字元(比如「>」和「<」)都将被转义为HTML实体(character entitiy),这行文本最终在浏览器中会被显示为文本形式的<script>alert(Bingo!)</script>。

在Python中,如果你想在单引号标记的字元串中显示一个单引号,那么你需要在单引号前添加一个反斜线来转义它,也就是把它标记为普通文本,而不是作为特殊字元解释。

在HTML中,也存在许多保留的特殊字元,比如大于小于号。如果你想以文本显示这些字元,也需要对其进行转义。

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。这个图片通过下面的方式显示:

<img src="{{ url }}">

类似的,{{url}}部分表示会被替换为用户输入的url变数值。如果不对输入的URL进行验证,那么用户可以将url设为「123"onerror="alert(Bingo!)」,最终的<img>标签就会变为:

<img src="123" onerror="alert(Bingo!)">

在这里因为src中传入了一个错误的URL,浏览器便会执行onerror属性中设置的JavaScript代码。

3.CSRF(Cross Site Request Forgery,跨站请求伪造)攻击

(1)攻击原理

CSRF攻击的大致方式如下:某用户登录了A网站,认证信息保存在cookie中。当用户访问攻击者创建的B网站时,攻击者通过在B网站发送一个伪造的请求提交到A网站伺服器上,让A网站伺服器执行相应的操作。

(2)攻击示例

假设我们网站是一个社交网站(Example Domain),简称网站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://example.com/account/delete就会删除账户。那么在攻击者的网站上,只需要创建一个显示图片的img标签,其中的src属性加入删除账户的URL:

<img src="http://example.com/account/delete">

当用户访问B网站时,浏览器在解析网页时会自动向img标签的src属性中的地址发起请求。你的账户就会被删除掉。

当然,现实中很少有网站会使用GET请求来执行包含数据更改的敏感操作,这里只是一个示例。

现在,假设我们吸取了教训,改用POST请求提交删除账户的请求。尽管如此,攻击者只需要在B网站中内嵌一个隐藏表单,然后设置在页面载入后执行提交表单的JavaScript函数,攻击仍然会在用户访问B网站时发起。

(3)主要防范措施

a.正确使用HTTP方法

在使用HTTP方法时,通常应该遵循下面的原则:

  • GET方法属于安全方法,不会改变资源状态,仅用于获取资源。
  • POST方法用于创建、修改和删除资源。在HTML中使用form标签创建表单并设置提交方法为POST,在提交时会创建POST请求。

正确使用HTTP方法后,攻击者就无法通过GET请求来修改用户的数据,下面我们介绍如何保护GET之外的请求。

b.CSRF令牌校验

要想避免CSRF攻击,关键在于判断请求是否来自自己的网站。

在前面我们曾经介绍过使用HTTP referer获取请求来源,理论上说,通过referer可以判断源站点从而避免CSRF攻击,但因为referer很容易被修改和伪造,所以不能作为主要的防御措施。

除了在表单中加入验证码外,一般的做法是通过在客户端页面中加入伪随机数来防御CSRF攻击,这个伪随机数通常被称为CSRF令牌(token)。

在HTML中,POST方法的请求通过表单创建。我们把在伺服器端创建的伪随机数(CSRF令牌)添加到表单中的隐藏栏位里和session变数(即签名cookie)中,当用户提交表单时,这个令牌会和表单数据一起提交。在伺服器端处理POST请求时,我们会对表单中的令牌值进行验证,如果表单中的令牌值和session中的令牌值相同,那么就说明请求发自自己的网站。

因为CSRF令牌在用户向包含表单的页面发起GET请求时创建,并且在一定时间内过期,一般情况下攻击者无法获取到这个令牌值,所以我们可以有效地区分出请求的来源是否安全。

对于AJAX请求,我们可以在XMLHttpRequest请求首部添加一个自定义栏位X-CSRFToken来保存CSRF令牌。

如果程序包含XSS漏洞,那么攻击者可以使用跨站脚本攻破可能使用的任何跨站请求伪造(CSRF)防御机制,比如使用JavaScript窃取cookie内容,进而获取CSRF令牌。

你应该列出一个程序安全项目检查清单,可以参考OWASP Top 10或是CWE(Common Weakness Enumeration,一般弱点列举)提供的Top 25(https://cwe.mitre.org/top25/)。确保你的程序所有的安全项目检查,也可以使用漏洞检查工具来,比如OWASP提供的WebScarab(https://github.com/OWASP/OWASP-WebScarab)。

推荐阅读:

相关文章