Session 无意劫持 Rails 4.2.4 (Devise / Warden), Phusion Passenger 5.0.24
Unintentional Session Hijacking in Rails 4.2.4 (Devise / Warden), Phusion Passenger 5.0.24
背景详情
我们最近遇到了一个问题,用户 A 可能无意中劫持了用户 B 的 session,而用户 B 正试图与用户 A(几乎)同时访问 Controller-generated 下载。
我们仍然不能 100% 确定发生这种情况的所有必要条件,但我们可以在我们的生产和暂存环境中可靠地重现该问题。这些环境的重要细节如下。
环境详细信息
应用程序服务器:Phusion Passenger 5.0.21 或 5.0.24(这意味着我们尝试了两个版本并且都重现了问题)
框架:Rails 4.2.4
语言:Ruby 2.2.3
操作系统:CentOS 6
有趣的是,我们可以 NOT 使用 Phusion Passenger 4.0.53.
重现这个问题
重现劫持的步骤
它可能看起来太简单以至于不可能是真的,但这就是必要的。
- 用户A登录系统
- 用户B登录系统
- 用户 A 和 B 都(几乎)同时快速点击同一个下载按钮
这就是某人的 session 被无意劫持所需要的全部。 (关于 A 或 B 的 session 是否被劫持似乎是轮盘赌,尽管它可能并不像看起来那么随机。)
我们知道用户session被劫持了,因为我们可以在页面上看到当前session的用户名字和姓氏。
每次,一个用户 "becomes" 另一个用户。
如果用户访问角色不同,这也意味着您现在可能拥有不同级别的访问权限。例如,如果那是他们无意中劫持的 session,某人可能会突然成为管理员....
需要代码
最初似乎是 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的修改可能很好地解决了这个问题。
也许我们不应该修改它,但我们希望有人能深入了解 cache-control 参数如何能够突然让我们陷入另一个 session.
为了对此进行测试,您必须有一个映射到 Controller#sample_method 的路由,并且您必须有一个可用的按钮来单击以下载此文件。
我意识到我们指定我们想要一个 CSV 而不是返回一个 CSV,但是在这种情况下我用一个过程替换了我们的实际 CSV,因为我们的 CSV 是在一个单独的 class 中生成的。
上述环境中的上述代码将重现该问题。
其他依赖项
我们正在使用 设计 gem 进行用户身份验证。如果您要设置测试应用程序以尝试重现此问题,则需要 Devise 和两个帐户设置。
顺便说一下,您还需要两个人在两台不同的计算机上进行测试。你们都需要同时登录系统,并尝试同时多次点击按钮。
我意识到这个问题似乎 far-fetched,但它确实在我们的环境中表现出来。它需要特定版本的 Phusion Passenger、一组特定的 headers 和一个渲染块才能让它发生,但它确实发生了。 (具体代码列在需要代码部分。)
修复
好消息是可以通过代码解决此问题。我们能够在 format.csv 块中使用 #send_data 方法。
而不是其他代码块,我们只是按照这些行做一些事情:
format.csv {
send_data data_here, filename: filename, type: 'text/csv', disposition: 'attachment'
}
这是更简洁、更好的代码。但我们仍然担心存在某种更大的问题 - 无论是在 Passenger 中还是在我们的代码本身中。
想法?
也许社区中的专家可以解释这种无意的 session 劫持是如何发生的。
似乎 session cookie 没有正确地来回发送。 (我们的 session 没有使用数据库。)
尽管我们已针对此特定问题实例进行了修复,但我们不确定是否可能存在其他潜在问题(也许在 Passenger 中?)导致此问题首先出现。
好像是个很奇怪的问题。
另一方面,p说实话,我们用 headers 做的事情是个坏主意。
感谢您的见解!
您的 cache-control 语句允许缓存(它强制重新验证,即 browser/cache 不会直接从缓存中提供请求,但不会阻止返回缓存的响应),而默认缓存控件 headers rails 发出包含 'private' 不允许中间代理缓存(仍然允许浏览器缓存)。
鉴于响应可能包含 rails session cookie,缓存该响应并将其重新用于另一个用户会导致第二个用户从第一个用户那里获取 cookie。即使您使用的是数据库支持的 session 存储,您仍然会得到 cookie 来标识要使用数据库中的哪一行。任何时候显示私人内容时都需要非常小心缓存 headers.
passenger 版本相关的原因是 passenger 5 包含一个 http 缓存层。您的错误仍然存在于乘客 4 中,只是更难触发(例如公司代理背后的 2 个用户)。
几乎可以肯定,您应该将响应标记为私有,这意味着中间缓存(包括 passenger 中的缓存)不会缓存响应。 Phusion 写了一篇 blog post 更详细地描述了这一点。您也可以完全关闭 turbocaching - 鉴于默认情况下 rails 将所有响应标记为私有,它可能无论如何都不会在您的应用程序中做任何有用的事情。
背景详情
我们最近遇到了一个问题,用户 A 可能无意中劫持了用户 B 的 session,而用户 B 正试图与用户 A(几乎)同时访问 Controller-generated 下载。
我们仍然不能 100% 确定发生这种情况的所有必要条件,但我们可以在我们的生产和暂存环境中可靠地重现该问题。这些环境的重要细节如下。
环境详细信息
应用程序服务器:Phusion Passenger 5.0.21 或 5.0.24(这意味着我们尝试了两个版本并且都重现了问题)
框架:Rails 4.2.4
语言:Ruby 2.2.3
操作系统:CentOS 6
有趣的是,我们可以 NOT 使用 Phusion Passenger 4.0.53.
重现这个问题重现劫持的步骤
它可能看起来太简单以至于不可能是真的,但这就是必要的。
- 用户A登录系统
- 用户B登录系统
- 用户 A 和 B 都(几乎)同时快速点击同一个下载按钮
这就是某人的 session 被无意劫持所需要的全部。 (关于 A 或 B 的 session 是否被劫持似乎是轮盘赌,尽管它可能并不像看起来那么随机。)
我们知道用户session被劫持了,因为我们可以在页面上看到当前session的用户名字和姓氏。
每次,一个用户 "becomes" 另一个用户。
如果用户访问角色不同,这也意味着您现在可能拥有不同级别的访问权限。例如,如果那是他们无意中劫持的 session,某人可能会突然成为管理员....
需要代码
最初似乎是 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的修改可能很好地解决了这个问题。
也许我们不应该修改它,但我们希望有人能深入了解 cache-control 参数如何能够突然让我们陷入另一个 session.
为了对此进行测试,您必须有一个映射到 Controller#sample_method 的路由,并且您必须有一个可用的按钮来单击以下载此文件。
我意识到我们指定我们想要一个 CSV 而不是返回一个 CSV,但是在这种情况下我用一个过程替换了我们的实际 CSV,因为我们的 CSV 是在一个单独的 class 中生成的。
上述环境中的上述代码将重现该问题。
其他依赖项
我们正在使用 设计 gem 进行用户身份验证。如果您要设置测试应用程序以尝试重现此问题,则需要 Devise 和两个帐户设置。
顺便说一下,您还需要两个人在两台不同的计算机上进行测试。你们都需要同时登录系统,并尝试同时多次点击按钮。
我意识到这个问题似乎 far-fetched,但它确实在我们的环境中表现出来。它需要特定版本的 Phusion Passenger、一组特定的 headers 和一个渲染块才能让它发生,但它确实发生了。 (具体代码列在需要代码部分。)
修复
好消息是可以通过代码解决此问题。我们能够在 format.csv 块中使用 #send_data 方法。
而不是其他代码块,我们只是按照这些行做一些事情:
format.csv {
send_data data_here, filename: filename, type: 'text/csv', disposition: 'attachment'
}
这是更简洁、更好的代码。但我们仍然担心存在某种更大的问题 - 无论是在 Passenger 中还是在我们的代码本身中。
想法?
也许社区中的专家可以解释这种无意的 session 劫持是如何发生的。
似乎 session cookie 没有正确地来回发送。 (我们的 session 没有使用数据库。)
尽管我们已针对此特定问题实例进行了修复,但我们不确定是否可能存在其他潜在问题(也许在 Passenger 中?)导致此问题首先出现。
好像是个很奇怪的问题。
另一方面,p说实话,我们用 headers 做的事情是个坏主意。
感谢您的见解!
您的 cache-control 语句允许缓存(它强制重新验证,即 browser/cache 不会直接从缓存中提供请求,但不会阻止返回缓存的响应),而默认缓存控件 headers rails 发出包含 'private' 不允许中间代理缓存(仍然允许浏览器缓存)。
鉴于响应可能包含 rails session cookie,缓存该响应并将其重新用于另一个用户会导致第二个用户从第一个用户那里获取 cookie。即使您使用的是数据库支持的 session 存储,您仍然会得到 cookie 来标识要使用数据库中的哪一行。任何时候显示私人内容时都需要非常小心缓存 headers.
passenger 版本相关的原因是 passenger 5 包含一个 http 缓存层。您的错误仍然存在于乘客 4 中,只是更难触发(例如公司代理背后的 2 个用户)。
几乎可以肯定,您应该将响应标记为私有,这意味着中间缓存(包括 passenger 中的缓存)不会缓存响应。 Phusion 写了一篇 blog post 更详细地描述了这一点。您也可以完全关闭 turbocaching - 鉴于默认情况下 rails 将所有响应标记为私有,它可能无论如何都不会在您的应用程序中做任何有用的事情。