在我们深入研究 Rails 应用程序的扩展预生产检查清单之前,您可能会问,“ Brakeman 和持续的拉取请求审查还不够吗?”
当然,使用自动工具可能会发现 SQL 注入问题,这很好。他们一直在变得更好。但他们仍然可能会遗漏一些问题。例如,他们找不到应用程序逻辑中的漏洞。那么,通过使用 Brakeman、检查逻辑漏洞并制定通用规则以确保过去的问题不会重演,从而对安全性更有信心不是很好吗?
本指南通过阐明开放 Web 应用程序安全项目 (OWASP) 发现的 前 10 个漏洞 来涵盖最后两点。 Brakeman 已经涵盖了以下一些内容,但我已经包含了一些真实世界的示例和一般编码策略的建议,以提高安全性。当然,如果您只是想快速取胜而不是战略,请留意下面的“操作”部分。
A1 SQL注入
这是一个 来自开源项目的 SQL 注入漏洞, 现已修复:
comments, emails = params[:id].split("+")
Comment.update_all("state = '#{params[:state]}'", "id IN
(#{comments})") unless comments.blank?
这很容易通过两个参数进行 SQL 注入:
params[:state]
和
params[:id]
。攻击者可以将
comments.user_id
更新为她自己的用户 ID,这样她就拥有所有这些用户 ID,因此可能可以读取所有用户 ID:
comments, emails = params[:id].split("+")
Comment.update_all("state = '#{params[:state]}'", "id IN
(#{comments})") unless comments.blank?
这让我们想起了我们应该始终努力争取的两项关键政策:
- 考虑所有用户提供的参数和属性可能是恶意的。
-
切勿在 SQL 字符串中使用字符串变形 (
#{...}
),即使您完全确定插入的值是安全的。还要避免所有输入都由您自己控制的内部模型范围或方法。这是为了防止混淆谁负责转义用户输入,这应该始终是最终将字符串放入最终 SQL 的方法。
动作
-
了解
鲜为人知的 ARel 方法
中的 SQL 注入,如
User.order("#{params[:sortby]} ASC")
。 - 在整个项目中进行手动搜索,并在 备忘单 中查找方法。他们直接使用用户提供的值吗?
- 对 SQL 条件使用 Hash 或 Array 形式,将预期的整数转换为整数,并将其他参数列入白名单或清理。即使您确定用户无法影响参数,也请使用这些对策。这是第二道防线,使代码永不过时。其他开发人员可能会在六个月后使用用户提供的数据使用您的方法。
A1 其他注塑
让我们看一下 同一个提交的另一个片段 :
comments, emails = params[:id].split("+")
Comment.update_all("state = '#{params[:state]}'", "id IN
(#{comments})") unless comments.blank?
这很容易受到 Ruby 类注入的攻击。攻击者可以定义一个任意的
params[:type]
Ruby 类来响应
find()
和
update_attribute()
方法,然后通过
params[:state]
更新任意对象的状态。
这再次提醒我们,当所有参数从用户那里返回时,我们需要重新检查所有参数。
另一种类型是
命令行注入
,可能发生在 Rails 命令行方法(
%x[]
、
system()
、
exec()
…
)中。所以不要做
system("ls", params[:options])
。攻击者可能会使用这些运算符链接命令:
&
、
&&
、
|
,
||
, 等等。
动作
- 寻找 constantize 、 classify 和 safe_constantize 的使用。确保类名不能直接受用户影响。
-
针对命令行注入,对方法
%x[]
、system()
、exec()
和…
执行相同的操作。
A2 会话和 Cookie
简而言之,cookie(以及会话)可以被窃取、 重放 ,有时甚至可以被修改或读取。现在,Rails 会话 cookie 默认标记为 HttpOnly ,因此会话 cookie 不能再通过 XSS 漏洞被窃取。但如果您使用其他 cookie,它们也需要被标记。顺便说一句,Devise 的“记住我”功能是基于 cookie 的,并且已经标记为 HttpOnly。
动作
-
在整个项目中搜索
cookies
访问器。 -
为
cookies[:user_name]
、cookies.signed[:user_id]
或cookies.permanent[:login]
分配一个像cookies[:login] = {value: "user", httponly: true}
。
当这一切都完成后,用户仍然有可能重放她自己的 cookie 或修改它们。这就是为什么不要在会话或 cookie 中存储“状态”很重要。一个常见的例子是一个向导,您在其中将一次性优惠券添加到步骤 2 中的会话。如果用户在步骤 2 中复制了会话 cookie,她以后可以通过将 cookie 粘贴回去来重新使用该一次性优惠券进入浏览器。这通常只能发生在基于 cookie 的会话中。
此外,重要的是要记住
cookies[:user_name]
和
cookies.permanent[:login]
可以由用户修改。
动作
-
在整个项目中搜索
cookies
和session
访问器。如果代码在其中存储了一些东西,如果稍后将其粘贴回去,这个值是否会造成任何伤害?例如,在向导的后续步骤中、在下一个会话中或在完全不同的帐户中?同时,检查该值是否是秘密。用户应该知道吗? -
如果是这样,并且这是一个简单的 cookie(
cookies[:user_name]
,cookies.permanent[:login]
),一个签名的 cookie(cookies.signed[:user_id]
),或者会话未加密,请加密 cookie/会话或者不要把它存放在那里。 -
当您从
cookies[:user_name]
或cookies.permanent[:login]
读取一个值时,您是在重新验证该值吗?它可能已被用户修改。
如果应用程序仅支持 HTTPS,则很容易将 Secure 标志添加到会话 cookie 和所有其他 cookie,这样它就不会在从 HTTP 到 HTTPS 的重定向中泄露。
动作
-
再次搜索
cookies
访问器并将它们标记为“secure”:cookies[:login] = {value: "user", httponly: true, secure: true}
- 在 config/initializers/session_store.rb 中添加相同的标志。
A2认证
你可能会说,“我很好。我用 Devise,效果不错。”确实如此,但您仍然可以检查一些内容,还可以添加一些额外的安全措施。
动作
- 您可能在 ApplicationController 中进行了身份验证检查,因此每个新操作都经过身份验证。如果您正在进行全面审核,请确保身份验证过滤器不会在不该跳过的地方跳过。
- 密码可能太短、太简单,或者可能需要每两个月更改一次,但用户总是会切换到以前使用过的密码。那是没有安全感吗?取决于您的要求,但这绝对值得考虑。 这个 Devise 扩展 允许您在一段时间后使密码过期并存档密码,这样它们就不能再使用了。 Devise 配置还包括一个最小密码长度选项。 这是对更复杂密码的讨论和实现 。
- 检查您的策略以防止暴力破解。如果可能,使用 Devise 模块 在一些登录尝试失败后锁定用户。使用 Rack::Attack 对您选择的请求进行速率限制。或者使用 此设计扩展 将验证码添加到登录/注册/解锁页面。
A3 跨站脚本
首先,你需要一个逃跑策略。这意味着要弄清楚架构的哪一层负责转义以及应该转义什么。
让我们看一下 Discourse 中的第一个示例 ,它具有在 UI 中带有弹出框的轮询功能。 此拉取请求 表明了解谁负责转义很重要。通常,这是视图。但是,在这种情况下,应用程序呈现的 JSON 只会根据 JSON 上下文进行转义。但是错误消息将在 HTML 上下文中使用,而且这个表示层似乎不会(或不能)转义。
在下一个示例
中,我们将研究
html_safe
的用法和行为非常相似的方法
raw()
。
改之前第15行的这段代码没有错:
comments, emails = params[:id].split("+")
Comment.update_all("state = '#{params[:state]}'", "id IN
(#{comments})") unless comments.blank?
但它需要一些关于 SafeBuffer 如何工作的知识。 HTML 安全字符串 << 不安全字符串的结果是什么?您可能期望一个不安全的字符串。实际上它将是一个 HTML 安全字符串,但右侧将被转义。
所以在这个例子中一切都是正确的,但有点复杂,将来有人可能会尝试更多的
.html_safe
或
raw()
调用。因此,将其拆分并使用较少的
.html_safe
是对可维护性的一项很好的投资。
此代码中的实际漏洞 位于第 1 行 (见下文)和 第 25 行 :
comments, emails = params[:id].split("+")
Comment.update_all("state = '#{params[:state]}'", "id IN
(#{comments})") unless comments.blank?
这使用了
truncate()
,它在 Rails 版本 3.2.8 中没有转义,但将字符串标记为 HTML 不安全。在最新的 Rails 版本中,它转义了截断的字符串。第 25 行然后使用
<<
符号,可能是希望右侧(
truncated
)被转义。但是,两个 HTML 不安全的字符串连接起来会导致不安全的字符串,没有任何转义。然后它被标记为 HTML 安全,因此没有转义。
从中,我们了解到以下内容:
-
您将需要
raw()
方法和.html_safe
的策略。应该如何以及在哪里使用它?您必须 100% 确定该字符串不包含来自用户的注入代码。最好尽可能避免使用.html_safe
或raw()
,而是经常转义一次。 -
使用 Rails(或自己的)字符串助手时,请确保您知道它们的作用以及在迁移到新版本时它们是否发生了变化。例如,比较
3.2.8
和
4.2
中的
truncate()
。 - 逃跑谁负责?通常,它应该是表示层,因为它需要根据上下文(HTML、JSON 等)进行转义。但是,如果视图没有(或不能)转义,您可能必须已经在模型中转义。
动作
-
在项目中搜索
.html_safe
或raw()
并尽可能减少使用。 - 使您自己的文本助手对输入进行转义,以便您可以在视图中安全地使用它们。
-
将文本助手的(可能)意外行为添加到您的中央安全策略中,例如,在 SECURITY.md 文件中。此外,描述
.html_safe
和raw()
使用策略以及“谁负责”策略。
A4授权
您可能在第一级就获得了授权:用户只能看到他们自己的东西,管理员可以看到一切。然而,第二层变得更加复杂,而且往往是不完整的。
这是一个例子:
-
用户模型
接受用户权限的嵌套属性
:
accepts_nested_attributes_for :permission
。 -
Permission 模型具有用户允许的标志,
例如
add_users
。 -
控制器在针对管理员和普通用户的更新操作中使用
User.update_attributes(params[:user])
。 -
即使 UI 中没有复选框,普通用户也可以添加
<input type="checkbox" name="user[permission_attributes][add_users]"
添加到表单以获得管理员权限。这意味着您必须授权通过
value="1" checked="checked" />params[:user][:permission_attributes]
进行的更改。
尽管在这个例子中看起来很明显,但这个检查经常被遗忘。因此,针对类似问题审核您的代码是一项很好的投资。
动作
-
在您的代码中搜索
accepts_nested_attributes_for
,这通常会导致像这样的授权问题。尽可能避免使用此方法。 - 这有时也会发生在子对象(例如,帖子的评论)中,并结合具有相同角色的两个用户。在 CommentsController 中,我还必须确保该评论确实属于该帖子(以及该帖子属于该用户)。
- 授权清晰且可维护很重要。作为练习,假设您在一家新公司开始工作,并且您查看了 中央身份验证文件 。它对你来说是可以理解和维护的吗?如果不是,请将其与您的授权方案进行比较,并可能将其拆分一下。
- 您的授权过滤器是每个操作之前的中央方法(如 ApplicationController 中的 load_and_authorize_resource )还是开发人员必须为每个操作手动添加的东西?后者有时会增加它不会被添加到新的“内部”操作中的风险。
-
为最坏的情况做好计划:有人通过窃听会话 cookie 甚至密码来获得对管理员帐户的访问权限。在这种情况下,确保攻击者不能在应用程序中做太多事情。例如,要求重新输入密码或一次性安全代码(
例如
,通过
:paranoid_verification
)。
A5 安全配置错误
您的 Rails 应用程序是否配置错误在很大程度上取决于它的作用。但是您可以检查一些常规配置选项。
动作
- 现在 默认情况下,Rails 将发送新的 HTTP 标头以提高安全性 。在旧的 Rails 版本中,您可以使用 SecureHeaders gem 来执行相同的操作,甚至在最新的 Rails 中,您可以添加更多的标头。很高兴知道默认值和其他值的作用,因此请参阅 gem 页面 以获取更多说明。
-
您可能知道
Rails.application.config.filter_parameters
数组。花一两分钟时间查看是否所有敏感参数都已添加到此处。法律可能要求您将私人消息或患者数据保存在尽可能少的地方或至少在国内(如果您想使用来自国外的日志聚合服务)。请注意,
:password
还将过滤:password_confirmation
,因此您不必列出每个变体。 - 如果应用程序重定向到包含令牌的地址,您还应该将它们添加到 Rails.application.config.filter_redirect 数组。
-
此外,您应该确保您的 gem 源是 HTTPS 而不是
git://
或:github
。有关更多详细信息, 请参见此处 。
A6 敏感数据传输
咖啡店中用户旁边的人可能会窃听数据传输。因此,如果可以,请使整个应用程序仅支持 SSL。如果 HTTPS 和 HTTP URL 都可用,中间人仍然可以在后台删除所有安全链接,以使受害者保持 HTTP 版本。
另一个问题是浏览器不知道您的应用程序是仅 SSL 的。因此,更老练的攻击者可以通过在后台将所有流量转发到 HTTPS 版本来向用户提供 HTTP 版本。这就是 HSTS 存在的原因,您可能已经在上面 A5 部分的 SecureHeaders gem 页面上阅读过它。
动作
- 还要将所有 cookie 标记为“安全”,这样用户的浏览器就不会在不安全的 HTTP 请求中发送(会话)cookie(这会将她重定向到 HTTPS 版本)。
- 设置提醒以反复 检查您的 TLS/SSL 安全性 是否存在最新漏洞。
- 这也与安全存储有关,因为存储的内容可能会在以后传输,例如,通过日志聚合服务或备份。因此,您可以在应用程序级别加密个人身份信息,例如,使用 attire_encrypted 。
-
关闭敏感表单输入中的自动完成:
<input
。
type="email" name="email" autocomplete="off" />
A7 缺少功能级别访问控制
注意: OWASP Top 10 中的这一部分提醒我们在 A2 和 A4 部分中已经提到的内容:在每个操作中重新检查身份验证和授权,尤其是在隐藏或内部操作中。
A8 跨站请求伪造 (CSRF)
也许您已经测试过如果 Rails 的对策(真实性令牌)丢失会发生什么情况。您可能还尝试过针对 CSRF 的不同 Rails 升级策略 。好,让我们关注仍然经常发生的两个问题。
你们提供“记住我”身份验证功能吗?让我们测试一下如果您在勾选“记住我”复选框和 CSRF 事件的情况下登录会发生什么。转到创建内容的表单,填写并使用开发人员控制台删除真实性令牌(表单中的第一个元素,或者如果它是远程表单,则为名为 csrf-token 的元标记),发送它,然后查看在 Rails 的日志中。它应该说“无法验证真实性令牌”,但也不会创建该对象。如果表单中的对象仍然被创建,您会发现一个非常常见的 CSRF 漏洞。
发生了什么?错误的令牌使 Rails 通过清除或更新会话将当前用户注销(取决于 ApplicationController 中的
protect_from_forgery
配置,
protect_from_forgery with: :exception
的行为不同)。但是一个单独的 cookie 中的“记住我”功能让你再次登录,然后运行该操作。
动作
-
覆盖ApplicationController中的
handle_unverified_request
方法。 -
运行原始实现(
例如
,使用
super
),但也删除那个“记住我”cookie。
这是第二个常见问题:至于 API, Rails 文档 说,“大概,无论如何你都会有不同的身份验证方案。”好点,所以让我们确保主应用程序的用户会话在 API 中不起作用,大概没有 CSRF 保护。如果没有,请登录主应用程序,在浏览器中输入 API 的 URL,然后查看 API 是否对您进行身份验证。
如果成功,攻击者可能会对 API 进行 CSRF 攻击。使用 REST 工具测试非 GET API 操作,然后对不依赖于 cookie 的 API 使用不同的身份验证方案。
动作
-
我知道你已经检查过了,但让我们花点时间确保
rake routes | grep GET
的输出中没有路由。rake routes | grep GET
更改应用程序的状态。如果是这样,是否可以将它们转换为非 GET 以使用 Rails 的 CSRF 保护? -
Rails 文档
对不同格式的请求有些误导。默认情况下,它们也会在主应用程序中被选中。所以在你
skip_before_action
之前
:verify_authenticity_tokenskip_before_action
,确保该操作不会更改、删除或创建(重要的)内容。
:verify_authenticity_token
A9 使用具有已知漏洞的组件
我们并不总是有时间立即更新软件。使用 Rails 安全策略 一周可能会有所帮助。
动作
A10 未经验证的重定向和转发
我们需要验证它们,因为链接或重定向可能会将用户引导至外部页面以进行网络钓鱼或类似操作。这是一个 未经验证的重定向 示例:
comments, emails = params[:id].split("+")
Comment.update_all("state = '#{params[:state]}'", "id IN
(#{comments})") unless comments.blank?
redirect_to params
不再被允许是有充分理由的。如果传递一个字符串,它将重定向到该 URL。如果传递了一个哈希,它仍然容易受到攻击,因为
url_for
提供了一个
:host
选项。
动作
-
例如,如果您要验证用户网站的 URL,请确保过滤掉一些鲜为人知的方案:
data:text/html;charset=utf-8,://<script>alert(1)</script>
作为链接将运行该 HTML/JS 并验证为 URL,如果您只是检查是否包含://
。此链接也是如此:javascript:alert('://')
。 -
重定向允许完整的 URL 作为参数,因此请检查重定向字符串。然而,
redirect_to params[:return_to] unless params[:return_to] =~ /\Ahttp/
试图禁止外部 URL,但它会允许相对 URL 方案,例如//attacker.com
。此验证可能会有所帮助:params[:return_to] =~ %r(\A#{root_url})
。 -
检查所有接受 Hash 的 redirect_tos,用户可以在其中传递
:host
选项。
结论
虽然您可能已经使用自动工具发现了一些漏洞,但您可能已经意识到它们无法找到所有的安全问题。本指南应该为您详细列出在将新版本投入生产之前要检查的内容:检查应用程序逻辑中的代码漏洞,为应用程序准备最常见的攻击,以及添加一些额外的安全措施。
我们还制定了通用编码规则,这样过去的问题就不会重演。在中央位置(例如 SECURITY.md 文件)描述这些编码规则以及与您的应用程序相关的最常见攻击和漏洞。在此文件中,还添加了需要额外注意的 Rails 方法,因为它们的行为与预期略有不同。请记住在该文件中描述您针对 html_safe 方法(参见 A2)和针对 SQL 注入的策略。另一个好的策略是在直接使用用户输入时提出问题,例如“这个值可能是什么?”它可能是注入字符串、意外(类型)对象、另一个用户的 ID、nil、0、空字符串、哈希或数组?
这只是我所说的 Rails 安全策略的一部分。如果您有一点时间,您可以 用 Rails 安全策略开始一周 。