首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

21.1 SQL Injection

SQL注入

对于很多web应用来说,SQL注入(SQL injection)是头一号的安全问题。那么,什么是SQL注入,它是如何发生的?

假设一个web应用接收来源不可靠的字符串(譬如来自表单输入的数据),然后直接把这些字符串放进SQL语句。再假设这个应用程序没有对SQL元字符(譬如反斜线、单引号)做适当的处理,那么攻击者就可以在服务器上执行SQL语句,从而获得敏感数据、创建非法数据记录、乃至进行任何数据库操作。

举例来说,假如一个web邮件系统提供了搜索功能,用户可以通过表单输入一个字符串,然后应用程序就会列出标题与该字符串相同的所有邮件。在应用程序的模型对象中,可能会有类似这样的查询操作:

Email.find(:all,

:conditions => "owner_id = 123 AND subject = '#{params[:subject]}'")

这种做法是很危险的。如果一个恶意用户输入“’ OR 1 --”来执行查询,Rails在执行find()方法时就会忠实地把这个字符串替换到SQL语句中,于是得到这样一条SQL语句[1]

select * from emails where owner_id = 123 AND subject = '' OR 1 --''

OR 1这个条件始终为真;两个减号代表SQL注释的开始,后续的任何东西都会被忽略。于是,这个恶意用户就会得到数据库中所有邮件的列表。[2]

Protecting against SQL Injection

防御SQL注入攻击

如果只使用ActiveRecord定义好的方法(譬如attributes()、save()和find()等),并且在调用这些方法时也没有加上自定义的条件、限制或者SQL语句,ActiveRecord自会将数据中所有危险的字符都加上引号。譬如说,下列调用就不会受到SQL注入攻击。

order = Order.find(params[:id])

即便id值来自输入请求,find()方法也会给其中的SQL元字符加上引号,因此恶意用户能够做的也只是引发一个“记录无法找到”的异常。

但如果使用了自定义的条件、限制或者SQL语句,并且其中的数据来自(哪怕只是间接地)外部输入,你就必须确保这些外部数据不包含任何SQL元字符。常见的不安全查询包括:

Email.find(:all,

:conditions => "owner_id = 123 AND subject = '#{params[:subject]}'")

Users.find(:all,

:conditions => "name like '%#{session[:user].name}%'")

Orders.find(:all,

:conditions => "qty > 5",

:limit => #{params[:page_size]})

抵挡SQL注入的正确方法是:绝对不要用Ruby的#{…}机制直接把字符串插入到SQL语句中,而应该用Rails提供的变量绑定(bind variable)工具。譬如说,前面这个“邮件查询”的操作可以重写如下:

subject = params[:subject]

Email.find(:all,

:conditions => [ "owner_id = 123 AND subject = ?", subject ])

如果find()的参数不是一个字符串,而是一个数组,ActionRecord会使用其中的第一个元素作为SQL模板,将后续的元素依次填充到模板中的“?”占位符。每个字符串会被加上引号;如果其中的某个字符对于数据库适配器具有特殊含义,它也会被加上引号。

除了使用问号与数组之外,还可以给SQL语句中待绑定的变量命名,同时传入一个hash以便进行值替换。在原书第204页,我们会详细介绍这两种形式的占位符。

Extracting Queries into Model Methods

将查询变成模型对象的方法

如果需要在几个不同的地方以类似的选项执行同一个查询,就应该在模型类中创建一个方法来封装这个查询操作。譬如说,应用程序中可能常常见到这样一个查询:

emails = Email.find(:all,

:conditions => ["owner_id = ? and read='NO'", owner.id])

所以,最好是将其封装成Email模型类的一个类方法。

class Email < ActiveRecord::Base

def self.find_unread_for_owner(owner)

find(:all, :conditions => ["owner_id = ? and read='NO'", owner.id])

end

# ...

end

在需要查找未读邮件的地方,只要直接调用这个方法即可。

emails = Email.find_unread_for_owner(owner)

如果用这种方式编程,就无需担心SQL元字符的影响——所有安全问题都被封装在模型类的层面上,只要确保这些模型方法不出问题,即便用外来数据作为参数调用它们也大可放心。

同时请记住,Rails会自动为模型对象的所有属性生成查询方法,这些查询方法都不会受到SQL注入攻击。所以,如果你想要查找属于特定用户、具有特定主题的邮件,完全可以直接使用Rails自动生成的方法。

list = Email.find_all_by_owner_id_and_subject(owner.id, subject)


[1] 实际的攻击方式取决于服务器所使用的数据库,本章所使用的例子适用于MySQL数据库。

[2] 当然,在真实应用中,owner_id是动态插入SQL语句的。为了保持例子的简单,我们直接将其写在代码中了。