安全的默认值对于构建安全系统至关重要。如果开发者必须采取各种明确行动才能实现安全,最终即使最有经验的开发者也会忘记这么做。出于这个原因,安全专家说:
“不安全的默认值导致系统不安全。”
Rails作为相对安全的Web框架的名声是当之无愧的。对常见的攻击都有直接可用的防护:跨站脚本(XSS),跨站请求伪造(CSRF)和SQL注入。Rails核心开发成员的安全知识丰富,且关注安全。
然而,也有地方的默认行为可以做的更安全。这篇文章探讨了Rails 3存在的潜在安全性问题,一些已在Rails 4修复,一些仍然有风险。我希望这篇文章能帮助你保护自己的应用程序,同时也能激发Rails本身的改变。
Rails 3的问题
我们以主干中已修复的Rails 3问题来开始。修复这些问题时,Rails团队功不可没,但是仍然值得注意,因为很多程序将在Rails 2和3上运行多年。
1. 通过#match路由泄漏进行CSRF攻击
这里有一个例子,直接来自Rails 3生成的config/routes.rb文件:
1 | WebStore::Application.routes.draw do |
3 | match 'products/:id/purchase' => 'catalog#purchase' , |
这样做的结果,对于任何HTTP方法(GET,POST等),都将/products/:id/purchases路由到CatelogController#purchase方法。问题是,Rails的跨站请求伪造(CSRF)防护对GET请求无效。从执行CSRF防护的方法中看到:
2 | !protect_against_forgery? || |
4 | form_authenticity_token == |
5 | params[request_forgery_protection_token] || |
6 | form_authenticity_token == |
7 | request.headers[ 'X-CSRF-Token' ] |
第二行跳过了CSRF检查:意味着如果request.get?为true,请求被认为是“verified”,CSRF检查被跳过。实际上,Rails源代码里,在该方法的前面就有注释:
Get应该是安全和无副作用的。
在程序里,你可以一直使用POST方法来访问/products/:id/purchase。但是因为路由器也允许GET请求,对于任何由#match路由的方法,攻击者都可以绕过CSRF保护。如果你的程序使用旧的通配符路由(已经不推荐),CSRF保护完全无效。
最佳实践: 对于不安全行为,不要使用GET。不要使用#match来添加路由(而应该用#post,#put等代替)。确保没有通配符路由。
解决方法: 现在,如果使用#match来添加路由,Rails需要你指定HTTP方法或via: :all。自动生成的config/routes.rb不再包含注释掉的#match路由。(通配符路由也被删除。)
2. 正则表达式中进行格式验证的锚点
观察下面的验证代码:
1 | validates_format_of :name , with: /^[a-z ]+$/i |
这是一个不明显的bug。开发者可能想强制要求整个name属性仅仅包含字母和空格。然而,它仅仅强制name属性至少一行由字母和空格组成。再看几个正则表达式匹配的例子来澄清一下:
1 | >> /^[a-z ]+$/i =~ "Joe User" |
4 | >> /^[a-z ]+$/i =~ " '); -- foo" |
7 | >> /^[a-z ]+$/i =~ "a\n '); -- foo" |
开发者其实应该用\A(字符串起始)和\z(字符串结束)锚点来代替^(行起始)和$(行结束)。正确的代码应该为:
1 | validates_format_of :name , with: /\ A [a-z ]+\z/i |
你可能会说开发者错了,而你做对了。然而,正则表达式锚点的行为并不显而易见,尤其对于没考虑多行输入的开发者。(也许该属性仅仅漏出一个文本输入区input field,而不是文本框textararea)
Rails在保护开发者方面考虑的不错,这也是在Rails 4中所做到的。
最佳实践:在任何情况下,使用\A和\z作为正则表达式锚点,代替^和$。
修复方法: Rails
4为validates_format_of引入了支持多行的选项。如果你的正则表达式使用^和$而不是\A和\z,且不传递multiline:
true,则Rails将抛出异常。这是创建安全默认行为的例子,且仍然在必要的场合提供控制选项来覆盖它。 3. 点击劫持
点击劫持或“UI纠正攻击”包括在一个可视框架内渲染目标站点,当受害者点击时就能欺骗他采取意想不到的行动。如果一个站点易受点击劫持攻击,一个攻击者可能欺骗用户采取非预期的行动,像一键购买,在Twitter上关注某人或改变他们的隐私设置。
为了抵御点击劫持攻击,一个站点必须防止自己被呈现在一个框架或它不能控制的iframe中。老的浏览器需要丑陋的“框架破坏”JavaScripts,
而现代的浏览器支持可以指示浏览器是否应该允许该网站被加框的X-Frame-Options
HTTP头。这个头很容易被包含,并且不可能破坏大部分网站,因此Rails应该默认包含了它。
最佳实践: 通过Twitter使用安全头RubyGem添加一个值为SAMEORIGIN或DENY的X-Fram-Options头。
修复方法: Rails4默认将X-Frame-Options头的值设为SAMEORIGIN。
1 | X -Frame-Options: SAMEORIGIN |
这告诉浏览器你的应用程序只能被源自相同域的页面加框。
4. 用户可读回话
默认的Rails 3会话存储使用署名的、未加密的cookies。虽然这可以包含会话不被篡改,对于攻击者来说,解码一个会话cookie的内容是轻而易举的事:
01 | session_cookie = <<- STR .strip.gsub(/\n/, '' ) |
02 | BAh7CEkiD3Nlc3Npb25faWQGOgZFRkkiJTkwYThmZmQ3Zm |
03 | dAY7AEZJIgtzZWtyaXQGO…--4c50026d340abf222… |
06 | Marshal.load(Base64.decode64(session_cookie.split( "--" )[ 0 ])) |
在一个会话中存储任何敏感信息都是不安全的。希望这是众所周知的,但即使一个用户的会话不包含敏感信息,也还是会带来奉献。通过解码会话数据,一个攻击者
能够获取一些关于程序的内部结构的、有利于攻击的信息。例如,他可能知道系统用的是什么认证(Authlogic,Devise等待)。
虽然不会创建一个自身的弱点,但它可以帮助攻击者。任何有关应用程序如何工作的信息都可用来磨练功绩,有时也用来避免触发那些会给开发人员预警攻击正在进行的异常或绊网。
用户可读会话违反最小特权原则,因为即使会话数据必须传给访问者的浏览器,他也不需要能够读取这些数据。
最佳实践: 不要将任何你不想让攻击者访问的信息放到一个会话中。
修复方法: Rails 4将默认会话存储改为加密的。在没有解密密钥的情况下,用户在客户端无法解码会话的内容。 |