#178 7 Security Tips
- Download:
- source codeProject Files in Zip (193 KB)
- mp4Full Size H.264 Video (21.8 MB)
- m4vSmaller H.264 Video (15.2 MB)
- webmFull Size VP8 Video (41.2 MB)
- ogvFull Size Theora Video (31.5 MB)
在这集railscast中,我们将介绍在Rails项目中遇到的七个安全问题。可能有些在以前的视频中已经介绍过,但是这次我们将全部介绍一下,这样你就可以完整地学习并重新确认你的Rails项目是否是安全的。
我们马上讲的这个项目是一个很简单的安全管理应用。用户可以注册、登录、创建项目。
每个应用都有一个图片和一些相关的辅助任务
我们将一直用这个应用来展示这七个安全问题,并且讲述怎样解决这些安全问题。
技巧1:大量的分配
我们先讲这个技巧,是因为它是我们要考虑的最重要的安全问题。也许它可能被很简单地忽略了,但它确实会使我们的应用数据不太安全。
当我们编辑模型对象project的时候,它只有name
和photo
两个属性。
当表单被提交到ProjectsController
的update方法时,会有很多属性被更新。
def update @project = current_users.projects.find(params[:id]) if @project.update_attributes(params[:project]) flash[:notice] = "Successfully updated project." redirect_to @project else render :action => 'edit' end end
表单中所有的参数通过哈希被传到模型project中去更新。因此一个用户可以更新它的所有属性,甚至有可能是相关联对象的属性。如果模型project关联了tasks(has_many:tasks
),这意味着模型project关联的tasks可以被改变。
这种通过大规模的分配来侵入的方式通常会用像curl
这样的工具发送POST请求到服务器,下面我们在控制台中看看它是如何被完成的。
首先、我们读取主键是2的模型project,它并没有关联tasks
>> p = Project.find(2) +----+--------+--------+--------+--------+--------+--------+--------+--------+ | id | name | cre... | upd... | pho... | pho... | pho... | pho... | use... | +----+--------+--------+--------+--------+--------+--------+--------+--------+ | 2 | Coo... | 200... | 200... | | | | | 2 | +----+--------+--------+--------+--------+--------+--------+--------+--------+ 1 row in set >> p.tasks => []
模型project中的其中一个属性是task_ids
,它存储了一组task主键id。假设我们可以用update_attributes
来改变模型project关联的tasks,这样我们就能把其他模型project关联的tasks关联到我们的模型project。
>> p.update_attributes(:task_ids => [4]) => true >> p.tasks +----+-----------------+-------------------+-------------------+------------+ | id | name | created_at | updated_at | project_id | +----+-----------------+-------------------+-------------------+------------+ | 4 | Sweep the yard. | 2009-09-08 20:... | 2009-09-08 20:... | 2 | +----+-----------------+-------------------+-------------------+------------+ 1 row in setThe task with the id of 4 now belongs to our project. >> p.tasks +----+-----------------+-------------------+-------------------+------------+ | id | name | created_at | updated_at | project_id | +----+-----------------+-------------------+-------------------+------------+ | 4 | Sweep the yard. | 2009-09-08 20:... | 2009-09-08 20:... | 2 | +----+-----------------+-------------------+-------------------+------------+ 1 row in set
很显然这是一个很危险的安全隐患。但是,我们可以通过模型中的attr_accessible
来控制我们想访问的属性。
class Project < ActiveRecord::Base belongs_to :user has_many :tasks has_attached_file :photo attr_accessible :name, :photo end
用上面这行代码就没人可以通过一个请求来改变task_ids
和模型project的其他属性了,只有name
和photo
能被更新。更多细节请观看或者阅读railscast 26。
技巧2:文件上传
在我们的例子应用中,用户可以为模型project上传图像。我们正在使用插件paperclip来管理文件,默认情况下可以上传任何文件,不仅仅限于图像。假设我们的应用在apache+passenger上运行,并且apache服务器被设置成可以执行PHP文件。如果我们上传PHP文件而不是一个图像,根据这个设置,会发生什么呢?
<?php phpinfo() ?>
当我们上传了这个文件,我们在上传图像的那个地方会看到一个损坏的图像。
但是如果我们用菜单在一个新标签页中打开这个图像,这个文件将被在服务器上执行。
这是一个很重要的安全漏洞,并且这意味着一个用户可以在浏览器中随心所欲地执行任何脚本。如果我们正在apache上运行Rails应用,并且可以执行PHP脚本和CGI脚本,我们需要小心注意文件上传。
当我们正在用插件paperclip来管理上传的文件,我们可以给相关模型加一行像这样的代码来限制文件类型。
validates_attachment_content_type :photo, :content_type => ['image/jpeg', 'image/png']
这样能确保只有jpeg和png的内容类型(content type)才能上传.然而,这并不是一种最终的解决方案,当上传文件的时候完全有可能伪造内容类型(spoof the content type)。文件扩展名也应该被检查,确保是不是匹配我们允许的内容类型。另外,还要更改apache的配置文件,从文件夹到文件上传的地方都不能执行脚本。
技巧3:过滤日志参数
我们的应用有注册和登录表单,在那里我们可以填写用户名和密码。默认情况下,Rails会以普通文本的形式保存所有的表单参数,这意味着当我们登录的时候,我们的用户名和密码被保存在日志文件里。
Processing UserSessionsController#create (for 127.0.0.1 at 2009-09-10 20:52:16) [POST] Parameters: {"commit"=>"Log in", "user_session"=>{"username"=>"eifion", "password"=>"pass"}, "authenticity_token"=>"GepjvVMhxroWGfE2NX4CZTtw6wLzUyd1b+Rm88qXI5g="}
当我们在数据库中加密我们的密码的时候,其实他们还是在日志里可以看到。幸运的是这可以很容易被纠正。在我们的ApplicationController
中有一行代码被注释掉了。
# filter_parameter_logging :password
取消这个注释,我们将会过滤日志文件中的密码参数。如果有其他字段需要过滤,我们可以把这些字段加入到参数列表当中。
现在当我们登录的时候,密码在日志文件中被过滤掉了。
Processing UserSessionsController#create (for 127.0.0.1 at 2009-09-10 20:55:24) [POST] Parameters: {"commit"=>"Log in", "user_session"=>{"username"=>"eifion", "password"=>"[FILTERED]"}, "action"=>"create", "authenticity_token"=>"cuI+ljBAcBxcEkv4pbeqLTEnRUb9mUYMgfpkwOtoyiA=", "controller"=>"user_sessions"}
技巧4:跨站点 伪造请求保护
这个技巧仍然围绕着ApplicationController
,具体指这行代码:
protect_from_forgery # :secret => '2b964d30ac961dfe405b234c10a42505'
有必要检查一下这行代码是否存在并且没有被注释掉。默认情况下应该是这样,这行代码会保护你的应用防止跨站点伪造请求。为了看清楚它是如何运行的,我们看一下project表单编辑的部分代码。
<form action="/projects/1" class="edit_project" enctype="multipart/form-data" id="edit_project_1" method="post"> <div style="margin:0;padding:0;display:inline"> <input name="_method" type="hidden" value="put" /> <input name="authenticity_token" type="hidden" value="cuI+ljBAcBxcEkv4pbeqLTEnRUb9mUYMgfpkwOtoyiA=" /> </div>
在表单元素中,Rails自动加入一个叫做authenticity_token
的隐藏字段。这个字段的值是用户session的唯一键值key,是基于一个随机字符串被保存在session中。对于任何一个单独的 POST,PUT,DELETE请求,RAILS都将自动检查这个键值。这会保证每个请求都是由我们自己网站上的用户发出的,而不是由其他站点的伪装用户发出的。对于GET请求,这个token不会被检查,所以我们需要确认GET请求不会改变或者删除我们应用的数据。
技巧5:授权
如果看一下我们应用中的页面,我们可以看到模型project的id在URL中,每一个用户可以拥有他们自己的模型project,我们要确保一个用户不能查看其他用户的模型project。
模型project 1(主键id是1)是我们自己的对象,如果我们在URL上改变主键号我们可以看到其他人的模型project。
为了修改这个bug,我们需要进入我们的ProjectsController
控制器,特别是读取数据库的那段代码。
def show @project = Project.find(params[:id]) end
这段代码将通过id
号读取模型project,而且没有检查这个project对象是否属于当前登录用户。我们有很多方法修改这个bug,但最简单的方法是用ActiveRecord关联的方式去得到当前用户的模型project。模型对象project属于一个user用户,我们可以修改代码做到这一点。
def show @project = current_user.projects.find(params[:id]) end
这将限定模型project的查询范围,它只属于当前登录用户。现在如果我们试着查看其他用户的模型project,我们会看到一个错误。
注意,在错误信息的SQL条件中,有当前用户的user_id。当应用运行在生产模式下,RecordNotFound
异常将会被转到404错误页面。
技巧6:SQL注入
也许你可能对SQL注入很熟悉了,所以我们尽量缩短这个介绍。
在Projects的index页面,我们有一个搜索表单,问题来了....输入的搜索项被直接输入到控制器中的一个SQL条件中。
def index @projects = current_user.projects.all(:conditions => "name like '%#{params[:search]}%'") end
很明显这么写代码不是一件很好的事情,对我们的应用来说,这样会给SQL注入攻击留下很大的空间。在搜索的时候,这段代码加了一个单独的引号,一个恶意的用户会看到他们没有权限看到的数据,甚至可能从数据库中修改或者删除数据。
显然这是一件很糟糕的事,所以我们不应该把搜索项直接传入到条件中,我们应该使用提问标示方式(question mark syntax)
def index @projects = current_user.projects.all(:conditions => ["name like ?", "%#{params[:search]}%"]) end
现在我们用数组的形式传入条件,而不是一个字符串。第一个参数是搜索条件,它可以被提问标示(question mark syntax)替换,第二个参数是我们想要传入的值。这种方式能引用这个参数并且避免一些特殊字符。
如果我们重新刷新这个页面,我们根本看不到搜索结果。
更多关于SQL注入的文章请观看或者阅读railscast 25
技巧7:HTML注入
当我们做项目的时候,我们忘记了把显示任务名字这个功能转义。这意味着如果一些人在<script>
标签中输入一些javascript代码,这个脚本将被执行。
每次这个页面重新加载的时候,这个脚本都会被执行,这让我们极其地不爽,而且还有可能从我们的站点得到一些敏感数据,并且这些数据会在你毫不知晓的情况下被转移到其他服务器上。
我们现在要做的是使用h方法包装用户的输入并且转义输出。
<%= h(task.name) %>
这样能确保任何HTML页面都将被转义,因此所有输入的脚本只能被显示,而不能运行。
当在页面中显示的时候,确保所有用户输入都被转义,这样可以防止跨站点脚本攻击。如果你想允许某些HTML标签可以显示,你可以用sanitize方法而不是h方法。
Rails 3 将自动转义页面中的内容,再加入h方法可能就不是那么重要了,但是除非Rails 3发行了release版本,不然我们仍然需要在我们应用中这么干。
以上是我们全部内容。希望通过全面了解这些安全技巧,你可以检查你的应用存在的安全隐患。如果想了解更多的信息,你可以看一下Ruby On Rails Security Guide,那里包含了很多东西,而且比我们这集railscast讲的更有深度。