Agile Web Development with Rails 翻译(七十八)

Agile Web Development with Rails 翻译(七十八)

21.5 不要想信ID参数

在我们前面讨论重新取回数据时,我们介绍了find()方法,它返回基于它的主键值的行。这个方法接受一个可选的哈希表参数,它可以被用于给返回的行添加额外的约束。


给出的主键唯一地识表内的一行,在使用这个键取回行时,为什么我们还希望应用额外的搜索条件?它是个非常有用的安全设备。

或许我们的应用程序让顾客查看它们的定单列表。如果顾客单击列表内的一个定单,应用程序显示定单的细节单击调用动作 order/show/nnn,此处的nnn是定单id

一个攻击者可能会注意到这个URL,并试图通过手工输入不同的定单id来查看其它用户的定单。我们可以通过在动作内使用一个强制的find()来防止这个。在这个例子中,我们用额外的条件,即定单的拥有者必须匹配当前用户来限制搜索。如果定单不匹配将抛出一个异常,我们通过在index页重新显示来处理它。

def show

id = params[:id]

user_id = session[:user_id] || -1

@order = Order.find(id, :conditions => [ "user_id = ?", user_id])

rescue

redirect_to :action => "index"

end

这个问题是没有对find()方法进行限制。基于从一个表单返回的id(ids)删除或毁掉行的动作也是很危险的。不幸地,delete()destroy()不支持额外的 :condictions参数。你将需要自己做些检查,首先读入行检查拥有者身份,或通过构造一个SQLwhere子句并传递它给delete_all()destroy_all()

另一种解决这个问题的方式是在你的应用程序内使用关联。如果我们声明一个user has_many orders,那么我们可以搜索只查找那个用户的定单,如

user.orders.find(params[:id])

21.6 不要暴露控制器方法

动作是控制器内的一个简单的public方法。这意味着如果你不小心的话,你可能暴露只在你的应用程序内部调用的动作方法。

有时候一个动作被用做帮助方法,但是不应该由最终用户直接调用。例如,邮件程序可能显示一个列表,它显示一个特定用户的所有邮件的主题行。接着,列表内的每个入口是一个Read E-Mail按钮。这些按钮使用一个URL连接回动作。如

http://website.domain/email/read/1357

在这个URL内,字符串1357是被读取的邮件id

当我们设计这个应用类型时,会很容易地忘记read()方法被公然地暴露了。在你的思想中,只有一种方式调用read(),那就是在用户从邮件列表单击连接时。

但是,一个喜欢冒险的用户可能查看URL,然后想知道在尾部手工地输入不同的数字会发生什么。除非你在写你的应用程序时时刻关心安全,否则这些用户将能够读取其它用户的邮件。read()的一个错误实现可能是

def read

@email = Email.find(params[:id])

end

这个方法返回给出id的邮件,而不管理邮件的拥有者。一种解决方式是添加对拥有者的测试。

def read

@email = Email.find(params[:id])

unless @email.owner_id == session[:user_id]

flash[:notice] = "E-Mail not found"

redirect_to(:action => "index")

end

end

(注意错误消息是如何被故意写成这样的;如果我们说,此邮件属于其它人的话,我们就给出了我们不应该与它人共享的信息。)

比在控制器内添加测试的更好方法是委派检查给模型。这种方式,我们可以重新安排些东西以便我们从不会读其它人的邮件到内存中。我们的动作方法会变成

def read

@email = Email.find_by_id_and_user(params[:id], session[:user_id])

unless @email

flash[:notice] = "E-Mail not found"

redirect_to(:action => "index")

end

end

这使用了一个动态生成的finder方法,它按id返回一个邮件,只要此id属于当前用户。

记住你的所有public方法都可以直接从浏览器中或使用手工输入HTML来调用。如果有请求的话,确保这些方法检验访问权限。

21.7 文件上传

一些面向社区的web站点允许它们的会员上传文件给其它会员下载。除非你小心,这些上传的文件可能被用于攻击你的站点。

例如,假设一些人上传以 .rhtml .cgi(或者是任何带有可被你的站点执行内容的扩展名)名字结尾的文件。如果你在下载页内直接连接这些文件,当你的webserver选择文件时可能被引诱执行它的内容,而不是简单的下载它。这就允许攻击者在你的服务器上运行任意代码。

解决方式是从不允许用户上传直接给随后其它用户直接下载的文件。相反,上传文件被放入在一个与你的web服务器无关目录内(Apache内是DocumnetRoot的外部)。那么,提供一个Rails动作来允许人们查阅这些文件,在这个动作内,要确保你

简单地确认请求的名字,有效的文件名匹配目录内一个现有的文件或表内的行。不要接受这样的文件名如 ../../etc/passwd (see the sidebar Input Validation Is Difficult)。你甚至可以存储上传文件在一个数据库表内并使用id,而不是名字来引用它们。

当你下载一个被显示在浏览器内的文件时,确保转义它包含的任何HTML序列,以消除潜在XSS攻击。如果你允许下载二进制文件,确保你设置适当的Content-type HTTP header ,以确保文件不会偶然地显示在浏览器内。这些描述在297页和350页。

21.8 不要缓存身份确认页面

记住页缓存会旁路你应用程序内的任何安全性的过滤器。如果你需要控制基于会话信息的访问,可使用动作或段缓存。31816.8节的缓存,第一部分和366页的缓存,第二部分有更多信息。

-------------------------------------------

输入确认是困难的

Johannes Brodwall 对本章写了下面回顾:

当你在确认输入时,在头脑中要记住下面重要事项:

确认是可靠的。有很多编码的点号和斜线会转义你的确认,但是它们会被基础系统解释。例如,../, .., %2e%2e%2f, %2e%2e%5c ..%c0%af (Unicode) 可以生成一个目录层。接受一个最小的字符集(试着从 [a-zA-Z][a-zA-Z0-9_]* 开始)

• Don’t try to recover from weird paths by replacing, stripping, and the like. For example, if you strip out the string ../, a malicious input such as ....// will still get though. If there is anything weird going on, someone is trying something clever. Just kick them out with a terse, non-informativemessage, such as “Intrusion attempt detected. Incident logged.”

我通常检查那个目录名(full_file_name_from_user)是否与期望的目录一样。这种方式我知道文件名是安全的。

-------------------------------------------------------------

21.9 Knowing That It Works

在我们希望确保我们写的代码完成我们想要的工作时,我们写测试。在我们想确保我们的代码安全时,我们应该做同样的事情。

如果你想确认你的新应用程序是安全的,就不要犹豫去做这些事情。使用Rails的功能测试来模仿潜在的用户攻击。你永远都会在你的代码里找到安全漏洞,写测试来确保修正它们。

同时,要认识到测试只能检查你已经完成的东西。攻击者的想法还是将会咬到你的。

posted @ 2007-02-17 10:50  海浪~~  阅读(170)  评论(0)    收藏  举报