跳到内容

Ruby on Rails 备忘单

简介

这份备忘单旨在为开发者提供快速基本的 Ruby on Rails 安全提示。它补充、增强或强调了 Rails 核心Rails 安全指南中提出的要点。

Rails 框架为开发者抽象了大量繁琐的工作,并提供了快速轻松完成复杂任务的方法。新开发者,那些不熟悉 Rails 内部工作原理的人,可能需要一套基本的指南来保护其应用程序的基本方面。本文档的预期目的是充当该指南。

项目

命令注入

Ruby 提供了一个名为“eval”的函数,它将基于字符串动态构建新的 Ruby 代码。它还有多种调用系统命令的方式。

eval("ruby code here")
system("os command here")
`ls -al /` # (backticks contain os command)
exec("os command here")
spawn("os command here")
open("| os command here")
Process.exec("os command here")
Process.spawn("os command here")
IO.binread("| os command here")
IO.binwrite("| os command here", "foo")
IO.foreach("| os command here") {}
IO.popen("os command here")
IO.read("| os command here")
IO.readlines("| os command here")
IO.write("| os command here", "foo")

虽然这些命令的功能非常有用,但在基于 Rails 的应用程序中使用它们时应格外小心。通常,这是一个糟糕的主意。如果需要,应使用允许的值列表,并尽可能彻底地验证任何输入。

RailsOWASP 的指南包含有关命令注入的更多信息。

SQL 注入

Ruby on Rails 通常与一个名为 ActiveRecord 的 ORM 一起使用,尽管它很灵活,可以与其他数据源一起使用。典型的非常简单的 Rails 应用程序使用 Rails 模型上的方法来查询数据。许多用例默认情况下就能防止 SQL 注入。但是,编写允许 SQL 注入的代码是可能的。

name = params[:name]
@projects = Project.where("name like '" + name + "'");

该语句是可注入的,因为 name 参数未转义。

这是构建此类语句的惯用写法

@projects = Project.where("name like ?", "%#{ActiveRecord::Base.sanitize_sql_like(params[:name])}%")

请注意不要根据用户控制的输入构建 SQL 语句。更实际和详细的示例列表在这里:rails-sqli.org。OWASP 提供了关于SQL 注入的广泛信息。

跨站脚本 (XSS)

默认情况下,XSS 防护是默认行为。当字符串数据显示在视图中时,它会在发送回浏览器之前被转义。这很有帮助,但在常见情况下,开发者会绕过此保护——例如为了启用富文本编辑。如果您想在 .erb 文件(Ruby 标记)中传递带有完整标签的变量到前端,很容易这样做。

# Wrong! Do not do this!
<%= raw @product.name %>

# Wrong! Do not do this!
<%== @product.name %>

# Wrong! Do not do this!
<%= @product.name.html_safe %>

不幸的是,任何使用 rawhtml_safe 或类似代码的字段都可能成为潜在的 XSS 目标。请注意,对于 html_safe() 也存在广泛的误解。

这篇说明详细描述了底层 SafeBuffer 机制。其他改变字符串输出方式的标签也可能引入类似问题。

字符串的 html_safe 方法命名有些令人困惑。它的意思是,我们确信字符串的内容可以安全地包含在 HTML 中而无需转义。这个方法本身是不安全的!

如果您必须接受用户的 HTML 内容,请考虑在应用程序中使用标记语言进行富文本(例如:Markdown 和 textile)并禁止 HTML 标签。这有助于确保接受的输入不包含可能恶意攻击的 HTML 内容。

如果您不能限制用户输入 HTML,请考虑实施内容安全策略以禁止执行任何 JavaScript。最后,考虑使用 #sanitize 方法,该方法允许您列出允许的标签。请小心,此方法已被证明存在多次缺陷,并且永远不会是完整的解决方案。

对于旧版本 Rails 来说,一个经常被忽视的 XSS 攻击向量是链接的 href

<%= link_to "Personal Website", @user.website %>

如果 @user.website 包含以 javascript: 开头的链接,则当用户点击生成的链接时,内容将执行。

<a href="javascript:alert('Haxored')">Personal Website</a>

较新版本的 Rails 会以更好的方式转义此类链接。

link_to "Personal Website", 'javascript:alert(1);'.html_safe()
# Will generate:
# "<a href="javascript:alert(1);">Personal Website</a>"

使用 内容安全策略 (CSP) 是另一项安全措施,用于禁止执行以 javascript: 开头的链接。

Brakeman 扫描器有助于在 Rails 应用程序中发现 XSS 问题。

OWASP 在顶级页面提供了关于 XSS 的更多一般信息:跨站脚本 (XSS)

会话

默认情况下,Ruby on Rails 使用基于 Cookie 的会话存储。这意味着除非您更改某些设置,否则会话不会在服务器上过期。这意味着某些默认应用程序可能容易受到重放攻击。这也意味着敏感信息绝不应放入会话中。

最佳实践是使用基于数据库的会话,值得庆幸的是,使用 Rails 可以很容易实现

Project::Application.config.session_store :active_record_store

有一份会话管理备忘单

认证

与所有敏感数据一样,通过在配置中启用 TLS 来保护您的身份验证

# config/environments/production.rb
# Force all access to the app over SSL, use Strict-Transport-Security,
# and use secure cookies
config.force_ssl = true

取消配置中第 3 行的注释,如上所示。

一般来说,Rails 本身不提供身份验证。但是,大多数使用 Rails 的开发者会利用 Devise 或 AuthLogic 等库来提供身份验证。

要启用身份验证,可以使用 Devise gem。

使用以下命令安装它

gem 'devise'

然后将其安装到用户模型

rails generate devise:install

接下来,在路由中指定哪些资源(路由)需要通过身份验证才能访问

Rails.application.routes.draw do
  authenticate :user do
    resources :something do  # these resource require authentication
      ...
    end
  end

  devise_for :users # sign-up/-in/out routes

  root to: 'static#home' # no authentication required
end

为了强制执行密码复杂性,可以使用 zxcvbn gem。使用它来配置您的用户模型

class User < ApplicationRecord
  devise :database_authenticatable,
    # other devise features, then
    :zxcvbnable
end

并配置所需的密码复杂度

# in config/initializers/devise.rb
Devise.setup do |config|
  # zxcvbn score for devise
  config.min_password_score = 4 # complexity score here.
  ...

您可以尝试此 PoC 以了解更多信息。

接下来,omniauth gem 允许多种身份验证策略。使用它可以配置与 Facebook、LDAP 和许多其他提供商的安全身份验证。请在此处阅读

令牌认证

Devise 通常使用 Cookies 进行身份验证。

如果希望使用令牌认证,可以使用 gem devise_token_auth 来实现。

它支持多种前端技术,例如 angular2-token。

这个 gem 的配置方式与 devise gem 本身类似。它也需要 omniauth 作为依赖。

# token-based authentication
gem 'devise_token_auth'
gem 'omniauth'

然后定义一个路由

mount_devise_token_auth_for 'User', at: 'auth'

并相应地修改用户模型。

这些操作可以通过一条命令完成

rails g devise_token_auth:install [USER_CLASS] [MOUNT_PATH]

您可能需要编辑生成的迁移文件,以避免根据您的用例产生不必要的字段和/或字段重复。

注意:当您只使用令牌认证时,控制器中不再需要 CSRF 防护。如果您同时使用两种方式:cookies 和令牌,则使用 cookies 进行认证的路径仍然必须防止伪造!

有一份身份验证备忘单

不安全直接对象引用或强制浏览

默认情况下,Ruby on Rails 应用程序使用 RESTful URI 结构。这意味着路径通常是直观且可猜测的。为了防止用户尝试访问或修改属于其他用户的数据,明确控制操作非常重要。在普通的 Rails 应用程序中,默认没有这种内置保护。可以在控制器级别手动完成此操作。

考虑使用基于资源的访问控制库,例如 cancancan (cancan 替代品) 或 pundit 来实现这一点也是可能的,并且可能更值得推荐。这确保了对数据库对象的所有操作都经过应用程序业务逻辑的授权。

关于此类漏洞的更多一般信息可在 OWASP Top 10 页面中找到。

CSRF (跨站请求伪造)

Ruby on Rails 对 CSRF 令牌有特定的内置支持。要启用它,或确保它已启用,请找到基本的 ApplicationController 并查找以下指令

class ApplicationController < ActionController::Base
  protect_from_forgery

请注意,这种控制的语法包含一种添加例外的方法。例外对于 API 或其他原因可能很有用——但应进行审查并有意识地包含。在下面的示例中,Rails 的 ProjectController 将不会为 show 方法提供 CSRF 保护。

class ProjectController < ApplicationController
  protect_from_forgery except: :show

另请注意,Rails 默认不为任何 HTTP GET 请求提供 CSRF 保护。

注意:如果您只使用令牌身份验证,则无需像这样在控制器中防范 CSRF。如果某些路径使用基于 Cookie 的身份验证,则仍然需要在这些路径上进行保护。

有一个顶级的 OWASP 页面用于 跨站请求伪造 (CSRF)

重定向和转发

Web 应用程序通常需要根据客户端提供的数据动态重定向用户。具体来说,动态重定向通常是指客户端在请求中包含一个 URL 作为参数发送给应用程序。一旦应用程序收到,用户就会被重定向到请求中指定的 URL。

例如:

http://www.example.com/redirect?url=http://www.example_commerce_site.com/checkout

上述请求会将用户重定向到 http://www.example.com/checkout。与此功能相关的安全问题是利用组织的受信任品牌来钓鱼用户,诱骗他们访问恶意网站,在我们的示例中是 badhacker.com

示例

http://www.example.com/redirect?url=http://badhacker.com

最基本但限制性最强的保护是使用 :only_path 选项。将其设置为 true 将基本上剥离任何主机信息。但是,:only_path 选项必须是第一个参数的一部分。如果第一个参数不是哈希表,则无法传入此选项。在没有自定义帮助器或允许列表的情况下,这是一种可行的方法

begin
  if path = URI.parse(params[:url]).path
    redirect_to path
  end
rescue URI::InvalidURIError
  redirect_to '/'
end

如果必须将用户输入与批准的站点列表或顶级域(TLD)通过正则表达式进行匹配,那么利用 URI.parse() 等库获取主机,然后将主机值与正则表达式模式进行匹配是很有意义的。这些正则表达式必须至少包含锚点,否则攻击者绕过验证例程的可能性会更大。

示例

require 'uri'
host = URI.parse("#{params[:url]}").host
# this can be vulnerable to javascript://trusted.com/%0Aalert(0)
# so check .scheme and .port too
validation_routine(host) if host
def validation_routine(host)
  # Validation routine where we use  \A and \z as anchors *not* ^ and $
  # you could also check the host value against an allowlist
end

此外,盲目重定向到用户输入参数可能导致 XSS。

示例代码

redirect_to params[:to]

将给出此 URL

http://example.com/redirect?to[status]=200&to[protocol]=javascript:alert(0)//

这类漏洞最明显的修复方法是限制到特定的顶级域名(TLD),静态定义特定的站点,或者将键映射到其值。

示例代码

ACCEPTABLE_URLS = {
  'our_app_1' => "https://www.example_commerce_site.com/checkout",
  'our_app_2' => "https://www.example_user_site.com/change_settings"
}

将给出此 URL

http://www.example.com/redirect?url=our_app_1

重定向处理代码

def redirect
  url = ACCEPTABLE_URLS["#{params[:url]}"]
  redirect_to url if url
end

OWASP 有一份关于未经验证的重定向和转发的更通用资源。

动态渲染路径

在 Rails 中,控制器动作和视图可以通过调用 render 方法动态地决定渲染哪个视图或局部视图。如果用户输入用于或作为模板名称的一部分,攻击者可能导致应用程序渲染任意视图,例如管理页面。

在使用用户输入来决定渲染哪个视图时应格外小心。如果可能,避免在视图的名称或路径中使用任何用户输入。

跨域资源共享

偶尔,需要与另一个域共享资源。例如,文件上传功能通过 AJAX 请求将数据发送到另一个域。在这些情况下,必须遵守 Web 浏览器遵循的同源规则。现代浏览器根据 HTML5 标准,将允许这种情况发生,但要实现这一点;必须采取一些预防措施。

当使用非标准 HTTP 结构时,例如非典型的内容类型头,以下适用:

接收站点应只列出允许发出此类请求的域,并在对 OPTIONS 请求和 POST 请求的响应中设置 Access-Control-Allow-Origin 头。这是因为 OPTIONS 请求是首先发送的,目的是确定远程或接收站点是否允许请求域。接下来,发送第二个请求,即 POST 请求。同样,必须设置头才能使事务显示为成功。

当使用标准 HTTP 结构时

请求被发送,浏览器在接收到响应后,会检查响应头以确定是否可以和应该处理该响应。

Rails 中的允许列表

Gemfile

gem 'rack-cors', :require => 'rack/cors'

config/application.rb

module Sample
  class Application < Rails::Application
    config.middleware.use Rack::Cors do
      allow do
        origins 'someserver.example.com'
        resource %r{/users/\d+.json},
        :headers => ['Origin', 'Accept', 'Content-Type'],
        :methods => [:post, :get]
      end
    end
  end
end

要设置一个头部值,只需在控制器内部(通常在 before/after_filter 中)将 response.headers 对象作为哈希访问。

response.headers['X-header-name'] = 'value'

Rails 提供了 default_headers 功能,它会自动应用提供的值。这适用于几乎所有情况下的大多数头部。

ActionDispatch::Response.default_headers = {
  'X-Frame-Options' => 'SAMEORIGIN',
  'X-Content-Type-Options' => 'nosniff',
  'X-XSS-Protection' => '0'
}

严格传输安全是一种特殊情况,它在环境文件(例如 production.rb)中设置

config.force_ssl = true

对于未处于前沿的开发者,有一个库(secure_headers)提供了相同行为,并抽象了内容安全策略。它将根据用户代理自动应用逻辑,生成一组简洁的头部。

业务逻辑漏洞

任何技术中的任何应用程序都可能包含导致安全漏洞的业务逻辑错误。业务逻辑漏洞很难或不可能使用自动化工具检测。预防业务逻辑安全漏洞的最佳方法是进行代码审查、结对编程和编写单元测试。

攻击面

一般来说,Rails 通过其 /config/routes.rb 文件避免了开放重定向和路径遍历等类型的漏洞,该文件规定了哪些 URL 应该可访问以及由哪些控制器处理。在考虑攻击面的范围时,路由文件是一个很好的查看点。

一个例子可能如下所示

# this is an example of what NOT to do
match ':controller(/:action(/:id(.:format)))'

在这种情况下,此路由允许任何控制器上的任何公共方法作为动作被调用。作为开发人员,您需要确保用户只能以预期的方式访问预期的控制器方法。

敏感文件

许多 Ruby on Rails 应用程序是开源的,并托管在公共可用的源代码仓库中。无论是这种情况,还是代码提交到公司源代码控制系统,都有某些文件应该被排除或仔细管理。

/config/database.yml                 -  May contain production credentials.
/config/initializers/secret_token.rb -  Contains a secret used to hash session cookie.
/db/seeds.rb                         -  May contain seed data including bootstrap admin user.
/db/development.sqlite3              -  May contain real data.

加密

Rails 使用操作系统加密。一般来说,自己编写加密代码总是一个糟糕的主意。

Devise 默认使用 bcrypt 进行密码哈希,这是一个合适的解决方案。

通常,以下配置会导致生产环境中有 10 次密码迭代:/config/initializers/devise.rb

config.stretches = Rails.env.test? ? 1 : 10

更新 Rails 和更新依赖项的流程

2013 年初,Rails 框架中发现了一些严重漏洞。那些落后于当前版本的组织在更新过程中遇到了更多麻烦,并做出了更艰难的决定,包括修补框架本身的源代码。

Ruby 应用程序普遍存在的另一个问题是大多数库(gem)都没有作者签名。实际上不可能使用来自受信任来源的库构建基于 Rails 的项目。一个好的实践可能是审计您正在使用的 gem。

总的来说,拥有一个更新依赖项的流程非常重要。一个示例流程可能定义三种机制来触发响应更新

  • 每月/每季度更新一次依赖项。
  • 每周都会考虑重要的安全漏洞,并可能触发更新。
  • 在特殊情况下,可能需要应用紧急更新。

工具

使用 brakeman,一个用于 Rails 应用程序的开源代码分析工具,可以识别许多潜在问题。它不一定能产生全面的安全发现,但可以找到容易暴露的问题。了解 Rails 潜在问题的一个好方法是查阅 brakeman 的警告类型文档。

一个新的替代方案是 bearer,一个适用于 Ruby 和 JavaScript/TypeScript 代码的开源代码安全和隐私分析工具,用于识别广泛的 OWASP Top 10 潜在问题。它提供了许多配置选项,并且可以轻松集成到您的 CI/CD 管道中。

正在出现一些工具可以用于跟踪依赖项集中的安全问题,例如来自 GitHubGitLab 的自动化扫描。

另一个工具领域是安全测试工具 Gauntlt,它基于 cucumber 构建,并使用 Gherkin 语法来定义攻击文件。

2013 年 5 月推出,与 brakeman scanner 非常相似的 dawnscanner rubygem 是一个用于 Rails、Sinatra 和 Padrino web 应用程序的安全问题静态分析器。1.6.6 版本有超过 235 个 Ruby 特定的 CVE 安全检查。