5

背景详情

我们最近遇到了一个问题,其中用户 A 可能无意中劫持了用户 B 的会话,用户 B 试图在(几乎)与用户 A 相同的时间访问控制器生成的下载。

我们仍然不能 100% 确定发生这种情况所需的所有条件,但我们可以在生产和暂存环境中可靠地重现该问题。这些环境的重要细节如下。

环境细节

应用服务器:Phusion Passenger 5.0.21 或 5.0.24(意味着我们尝试了两个版本并且都重现了问题)

框架:Rails 4.2.4

语言:Ruby 2.2.3

操作系统:CentOS 6

有趣的是,我们无法使用Phusion Passenger 4.0.53重现此问题。

重现劫持的步骤

这似乎太简单了,不可能是真的,但这就是所有必要的。

  1. 用户A登录系统
  2. 用户B登录系统
  3. 用户 A 和 B 都(几乎)同时快速单击同一个下载按钮

这就是某人的会话被无意劫持所需要的一切。(关于 A 或 B 的会话是否被劫持,这似乎是轮盘赌,尽管它可能不像看起来那样随机。)

我们知道用户的会话被劫持了,因为我们可以看到当前会话的用户的名字和姓氏显示在页面上。

每次,一个用户“变成”另一个用户。

如果用户访问角色不同,这也意味着您现在可能拥有不同级别的访问权限。例如,如果这是他们无意劫持的会话,某人可能会突然成为管理员....

需要代码

最初似乎 Phusion Passenger 是导致此问题的唯一原因,因为当我们切换回版本 4 时,此问题不再出现。

但是在一些代码更改之后,我们确定控制器代码中的一个方法似乎导致了这个问题的发生。

这是一个示例控制器方法,它将在 Phusion Passenger 5.0.21 或 5.0.24 上产生此问题:

def sample_method
  respond_to do |format|
    format.csv {
      headers.merge!({'Cache-Control'=>'must-revalidate, post-check=0, pre-check=0'})
      render :text => proc { |response, output|
        100.times do |i|
          output.write("This is line #{i}\n")
        end
      }
    }
  end
end  

看来我们对 Cache-Control 的修改可能很好地解决了这个问题。

也许我们不应该修改它,但我们希望有人能深入了解缓存控制参数如何能够让我们突然陷入不同的会话。

为了对此进行测试,您必须有一个映射到 Controller#sample_method 的路由,并且您必须有一个可用于单击下载此文件的按钮。

我意识到我们正在指定我们想要一个 CSV 而不是返回 CSV,但在这种情况下我用 proc 替换了我们的实际 CSV,因为我们的 CSV 是在一个单独的类中生成的。

上面列出的环境中的上述代码将重现该问题。

其他依赖

我们正在使用Devise gem进行用户身份验证。如果您要设置一个测试应用程序来尝试重现此问题,则需要设计和两个帐户设置。

顺便说一句,您还需要两个人在两台不同的计算机上进行测试。你们都需要同时登录系统并尝试同时多次单击该按钮。

我意识到这个问题似乎牵强附会,但它确实在我们的环境中表现出来。它需要特定版本的 Phusion Passenger、一组特定的标头和一个渲染块才能发生,但它确实发生了。(具体代码列在所需代码部分。)

修复

好消息是,有办法用代码解决这个问题。我们能够在 format.csv 块中使用 #send_data 方法。

而不是其他代码块,我们只是按照以下方式做一些事情:

  format.csv {
    send_data data_here, filename: filename, type: 'text/csv', disposition: 'attachment'
  }

这是更干净的代码和更好的代码。但是我们仍然担心存在某种更大的问题——无论是在乘客中,还是在我们的代码本身中。

想法?

也许社区中的专家可以解释这样的无意会话劫持是如何可能的。

似乎会话 cookie 没有正确地来回发送。(我们的会话没有使用数据库。)

尽管我们对这个特定的问题实例进行了修复,但我们不确定是否可能存在其他潜在问题(可能是在乘客中?)导致这个问题首先出现。

这似乎是一个非常奇怪的问题。

另一方面,也许只是我们对标题所做的事情是一个坏主意。

感谢您的见解!

4

1 回答 1

6

您的缓存控制语句允许缓存(它强制重新验证,即浏览器/缓存不会直接从缓存中提供请求,但不会阻止返回缓存的响应),而默认缓存控制标头 rails 发出包含'private' 不允许中间代理缓存(仍然允许浏览器缓存)。

鉴于响应可能包括 rails session cookie,缓存该响应并将其重用于另一个用户会导致第二个用户从第一个用户获取 cookie。即使您使用的是数据库支持的会话存储,您仍然会获得标识数据库中要使用的行的 cookie。每当您显示私有内容时,您都需要非常小心缓存标头。

乘客版本相关的原因是乘客 5 包含一个 http 缓存层。您的错误仍然存​​在于乘客 4 中,只是更难触发(例如,公司代理后面的 2 个用户)。

您几乎可以肯定将您的响应标记为私有,这意味着中间缓存(包括乘客中的缓存)不会缓存响应。Phusion 写了一篇博客文章更详细地描述了这一点。你也可以完全关闭涡轮缓存——因为默认情况下,rails 将所有响应标记为私有,它可能无论如何都不会在你的应用程序中做任何有用的事情。

于 2016-02-06T08:43:00.640 回答