Ruby on Rails 安全全景

1. Rails 安全模型概览

Rails 以"约定优于配置"著称,内置安全机制丰富:

  • Strong Parameters(强参数)防止 Mass Assignment;
  • CSRF Protection 默认启用;
  • BCrypt / Argon2 密码哈希;
  • Active Record 自动参数化;
  • Action View 默认 HTML 转义;
  • HTTP Security Headers(默认启用 hsts)。

漏洞根源:开发者主动"简化"或"绕过"这些安全机制。

2. 经典漏洞详解

2.1 Mass Assignment(Rails 4 之前)

Rails 3 时代的 attr_accessible / attr_protected 声明不够灵活,开发者常在开发阶段省略,导致攻击者通过额外的表单参数直接赋值数据库列。

漏洞代码(Rails 3):

class User < ActiveRecord::Base
  # 开发时常忘加 attr_accessible
  # attr_accessible :name, :email
end

# controller
def create
  User.create(params[:user])  # 直接把 params 全丢进去
end

PoC

POST /users HTTP/1.1
Host: victim.example.com
Content-Type: application/x-www-form-urlencoded

user[name]=alice&user[email]=alice@example.com&user[role]=admin
# role 字段被直接赋值!alice 变成管理员!

修复:Rails 4+ 强制 Strong Parameters:

private
def user_params
  params.require(:user).permit(:name, :email, :password)
  # ❌ 忘了 role
  # ✅ role 如果是用户自己设置的,放在单独的方法里手动校验
end

2.2 Asset Pipeline 反序列化

Rails 3 / 4 的 Asset Pipeline 会把编译后的文件缓存在 tmp/cache/assets。攻击者如果能控制文件内容(如通过 S3 上传 + 文件包含),就能触发 YAML / Marshal 反序列化 RCE。

CVE-2014-0130 就是这条链。

修复:升级到 Rails 5+,或用 Sprockets 4 + YAML.safe_load。

2.3 Active Record SQL 注入(Rails 5.1 之前)

Rails 的 where 方法支持 hash 形式和数组形式,但开发者常误用字符串形式:

# ❌ 危险
User.where("name = '#{params[:name]}'").first
User.find("id = #{params[:id]}")
User.order(params[:sort])  # 类似 Django order_by

# ✅ 安全
User.where(name: params[:name]).first
User.where("name = ?", params[:name]).first
# 白名单排序
ALLOWED_SORT = { 'id' => 'id ASC', 'name' => 'name DESC' }
User.order(ALLOWED_SORT.fetch(params[:sort], 'id ASC'))

2023 年仍在被利用的 Rails SQL 注入(GitHub Advisory GHSA-34f8-26hw-6wrg)where(foo: bar) 形式中 bar 为数组时存在绕过。

2.4 CVE-2020-8164 Action View 双重序列化

Rails 5.2.4.3 / 6.0.3 以下版本中,当控制器返回 JSON 时,若参数包含 action_dispatch.content_typing,会触发 Marshal.load 反序列化任意对象。

PoC

POST /users HTTP/1.1
Host: victim.example.com
Content-Type: application/json
Content-Length: 132

{"authenticity_token":"","action_dispatch.content_typing":"BAhvLUkAAnhIVTEEMQV5N2VybnNlYw5TbGVjdGlvblRlbXBsYXRlAApvYg10AAAAAAMKdmFsdWUAAmliAQFzNQx1dXRwYWdlBAAAAA=="}

其中 base64 解码后是:

Marshal.dump(ActionController::Parameters.new("value" => 1, "oops" => OpenStruct.new(eval: "system('id')")))

2.5 CVE-2022-32224 Web Console 路径穿越

Rails Web Console(开发环境)允许通过 /__better_errors/eval 执行任意 Ruby 代码,这在生产环境里被误部署后等同于 RCE。

修复:确保 Web Console 只在 development 环境启用。

2.6 CVE-2023-22667 Content Security Policy 绕过

Rails 7 的 image_tag 生成的 srcset 可被特殊 URL 绕过 CSP。

3. Rails 安全 Checklist

3.1 Gemfile 安全依赖

# 必须锁定版本,避免意外升级
gem 'rails', '~> 7.1', '>= 7.1.3'
gem 'devise'
gem 'bcrypt', '~> 3.1.7'
gem 'rack-attack'          # 速率限制
gem 'brakeman', require: false, group: :development
gem 'bundler-audit', require: false, group: :development
gem 'secure_headers'
gem 'audit-log'
gem 'rack-cors'

group :development, :test do
  gem 'web-console', '~> 4.2'
end

3.2 ApplicationController 保护

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  before_action :configure_permitted_parameters, if: :devise_controller?
  before_action :set_secure_headers

  private

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
  end

  def set_secure_headers
    response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains; preload'
    response.headers['X-Content-Type-Options'] = 'nosniff'
    response.headers['X-Frame-Options'] = 'DENY'
    response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
  end
end

3.3 rack-attack 速率限制

# config/initializers/rack_attack.rb
class Rack::Attack
  throttle('req/ip', limit: 300, period: 5.minutes) do |req|
    req.ip unless req.path.start_with?('/assets')
  end
  throttle('login/ip', limit: 5, period: 20.seconds) do |req|
    if req.path == '/users/sign_in' && req.post?
      req.ip
    end
  end
  self.throttled_response = lambda do |env|
    [429, { 'Content-Type' => 'application/json' }, [{ error: 'Too many requests' }]]
  end
end

3.4 secure_headers gem

# config/initializers/secure_headers.rb
SecureHeaders::Configuration.default do |config|
  config.hsts = "max-age=31536000; includeSubDomains; preload"
  config.xframe_options = "DENY"
  config.csp = {
    default_src: ["'self'"],
    script_src: ["'self'"],
    style_src: ["'self'", "'unsafe-inline'"],
    img_src: ["'self'", "data:", "https:"],
    frame_ancestors: ["'none'"],
  }
end

3.5 强制 HTTPS

# config/application.rb
config.force_ssl = true
# 同时设置 secure cookies
config.action_dispatch.cookies_same_site_protection = :strict

4. Brakeman 扫描

# Brakeman 是 Rails 最著名的静态分析工具
gem install brakeman
brakeman . -o report.html
brakeman --confidence-level=2 --no-pager

典型警告项:

  • Weak Authentication:密码哈希强度;
  • Unsafe Reflection:eval / send / instance_variable_set;
  • SQL Injection:动态拼接字符串;
  • Mass Assignment:缺少 Strong Parameters;
  • Information Disclosure:debug 模式泄露。

5. bundler-audit 依赖检查

gem install bundler-audit
bundler-audit check --update

6. 错误处理

# config/environments/production.rb
config.consider_all_requests_local = false
config.action_dispatch.show_exceptions = true
config.action_controller.raise_on_open_redirects = true
config.log_level = :warn
# 禁止泄露堆栈
config.active_record.verbose_query_logs = false

7. 数据库层加固

# 禁止 DDL 运行时修改
ActiveRecord::Base.class_eval do
  unless Rails.env.development?
    self.class_eval do
      def alter(); raise "Not allowed in production"; end
    end
  end
end

# SQL 白名单验证
ALLOWED = ['id', 'name', 'email']
scope :sorted, ->(field) {
  where.not(nil: true).order(ALLOWED.include?(field) ? field : 'id')
}

8. 升级路线图

| 版本 | 状态 | 安全建议 |
| Rails 5.2 | EOL 2021 | 立刻升级 |
| Rails 6.0 | EOL 2022 | 立刻升级 |
| Rails 6.1 | EOL 2023 | 升级到 7.0+ |
| Rails 7.0 | 安全支持中 | 升级到 7.1 |
| Rails 7.1 | 当前推荐 | 生产首选 |

9. 小结

Rails 安全 = Strong Parameters + secure_headers + rack-attack + brakeman + bundler-audit + force_ssl + 定期升级

Rails 框架团队的安全意识在所有主流框架中排第一,几乎每个安全功能(Strong Parameters、CSRF、HSTS)都在框架层面强制开启。真正的漏洞大多来自开发者绕过这些默认保护——比如开发时关了 CSRF、写了 raw SQL、或忘了 permit 参数。