关于 security
Rails Security Expert 是面向 Rails 开发的安全指导技能,提供从输入处理到会话、密钥和日志的安全编码建议。适用于实现或审查 Rails 功能的开发者,重点是默认启用防护并减少敏感信息暴露。
主要功能
- 01指导 CSRF 与 XSS 防护
- 02采用参数化查询并验证输入
- 03使用 strong parameters 限制写入字段
- 04提供身份验证与授权建议
- 05涵盖会话、密钥、CSP 和安全日志
适用场景
- 01实现接收用户输入的控制器或 API
- 02处理用户生成内容与文件上传
- 03配置认证、会话和访问控制
- 04审查或修复 Rails 安全问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ thinkoodle/ rails-skills --skill security安装后在 Claude Code 中输入/security确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/advanced-threats.mdauthentication.mdcredentials.mdcsp-headers.mdcsrf.mdfile-uploads.mdscanning-tools.mdsessions.mdsql-injection.mdxss.md详细介绍
Rails 安全专家
默认编写安全的 Rails 代码。安全错误是 Agent 可能犯下的最危险错误——XSS 漏洞或 SQL 注入可以立即危及所有用户。如有疑问,请选择更安全的方案。
理念
核心原则:
- 默认安全——Rails 内置了出色的防护机制。除非有明确且记录在案的理由,否则绝不要禁用这些机制
- 深度防御——层层叠加防护。不要依赖单一机制
- 允许列表优于拒绝列表——输入验证、URL 方案、HTML 标签和重定向目标应始终优先使用允许列表,而非屏蔽列表
- 绝不信任用户输入——在得到验证之前,每个参数、请求头、Cookie 和 URL 片段都应视为由攻击者控制
- 默认拒绝——授权情况不明确时,拒绝访问。输入可疑时,拒绝接受
- 尽量减少暴露——减少日志记录、减少信息暴露、减少数据存储。过滤敏感参数,避免泄露堆栈跟踪,静态加密
何时使用此技能
- 添加身份验证或授权逻辑
- 编写接收用户输入的控制器
- 在视图中渲染用户生成的内容
- 构建 API 端点
- 处理文件上传或下载
- 配置会话、Cookie 或 CSRF 防护
- 管理密钥和凭据
- 设置 HTTP 安全标头或 CSP
- 审查代码中的安全漏洞
- 修复已报告的 CVE 或安全问题
说明
步骤 1:审查现有安全状况
更改任何内容之前,先检查现有配置——了解现有安全状况可避免意外削弱防护:
# Check CSRF configuration
rg "protect_from_forgery" app/controllers/
# Check strong params usage
rg "params\.permit\|params\.require" app/controllers/
# Check for dangerous patterns
rg "\.html_safe|raw\(|raw " app/views/ app/helpers/
rg "\.where\(\".*#\{" app/ --type ruby
rg "redirect_to.*params" app/controllers/
# Check security headers
rg "content_security_policy" config/
rg "force_ssl" config/
# Check filter_parameters
cat config/initializers/filter_parameter_logging.rb
# Check credentials setup
ls config/credentials* config/master.key 2>/dev/null
步骤 2:CSRF 防护
绝不要禁用 CSRF 防护。 Rails 默认启用此功能——它是防御跨站请求伪造的主要手段。
# ApplicationController — this should ALREADY be here
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
工作原理:
- 每个非 GET 请求都需要真实性令牌
- 在布局的
<head>中加入<%= csrf_meta_tags %>,以支持 Turbo/Ajax - 使用令牌身份验证的 API 控制器可以跳过 CSRF 防护——但前提是它们不使用基于 Cookie 的会话(使用 Cookie 会话却不启用 CSRF 防护,会让攻击者伪造请求)
- 对提供浏览器会话的控制器,绝不要使用
skip_forgery_protection
# API-only controllers with token auth — acceptable to skip
class Api::BaseController < ActionController::API
# No cookie session = no CSRF needed
# But MUST authenticate via Authorization header
end
# WRONG — disabling CSRF on a browser-facing controller
class PaymentsController < ApplicationController
skip_forgery_protection # NEVER DO THIS
end
步骤 3:防止 XSS
Rails 默认会自动转义所有 ERB 输出。 这是防御 XSS 的主要手段——绕过它意味着任何由用户控制的字符串都能向其他用户的浏览器注入脚本。
# SAFE — auto-escaped (default)
<%= user.name %>
<%= user.bio %>
# DANGEROUS — bypasses escaping. Only use for trusted, sanitized HTML
<%= raw(content) %> # AVOID
<%= content.html_safe %> # AVOID
<%== content %> # AVOID
# CORRECT way to render trusted HTML — sanitize first
<%= sanitize(user.bio, tags: %w[p br strong em a ul ol li], attributes: %w[href title]) %>
html_safe 使用规范:
- 绝不要对用户输入调用
.html_safe——这会立即造成 XSS 漏洞 - 绝不要对通过插值包含用户数据的字符串调用
.html_safe——插值的值会绕过转义 - 只有对完全由你控制的字符串字面量,才可以使用
.html_safe - 在辅助方法中构建 HTML 时,使用
tag构建器或content_tag——它们会自动处理转义
# WRONG — XSS vulnerability
def greeting(user)
"<h1>Hello, #{user.name}</h1>".html_safe
end
# CORRECT — use tag helpers
def greeting(user)
tag.h1("Hello, #{user.name}")
end
# CORRECT — escape manually if you must build strings
def greeting(user)
"<h1>Hello, #{ERB::Util.html_escape(user.name)}</h1>".html_safe
end
sanitize() 的用法:
- 明确指定允许的标签和属性——默认允许列表可能比实际需要的范围更广
- 对于富文本,优先使用 Action Text,它会自动处理净化
步骤 4:防止 SQL 注入
绝不要将用户输入插入 SQL 字符串。 插值会让攻击者注入任意 SQL——他们可以读取、修改或删除数据库中的任何数据。
# DANGEROUS — SQL injection
User.where("name = '#{params[:name]}'")
User.where("email LIKE '%#{params[:q]}%'")
Order.where("status = '#{status}' AND user_id = #{user_id}")
# SAFE — parameterized queries
User.where("name = ?", params[:name])
User.where("email LIKE ?", "%#{User.sanitize_sql_like(params[:q])}%")
Order.where(status: status, user_id: user_id)
# SAFE — named parameters
User.where("name = :name AND role = :role", name: params[:name], role: params[:role])
# SAFE — hash conditions (preferred)
User.where(name: params[:name])
LIKE 查询需要特殊处理:
# WRONG — user can inject % and _ wildcards
User.where("name LIKE ?", "%#{params[:q]}%")
# CORRECT — sanitize LIKE wildcards
User.where("name LIKE ?", "%#{User.sanitize_sql_like(params[:q])}%")
不太明显的注入途径——这些方法也会将字符串直接传递给 SQL:
order()——绝不要传入原始用户输入(攻击者可以注入子查询)pluck()、select()、group()、having()、joins()的字符串参数find_by_sql()、connection.execute()Arel.sql()——只能与可信字符串一起使用,绝不要传入用户输入
# WRONG — order injection
Post.order(params[:sort])
# CORRECT — permit-list of allowed columns
allowed = %w[created_at title updated_at]
direction = %w[asc desc].include?(params[:dir]) ? params[:dir] : "asc"
column = allowed.include?(params[:sort]) ? params[:sort] : "created_at"
Post.order("#{column} #{direction}")
第 5 步:批量赋值保护
每次写入模型时都要使用强参数。 原始参数会让攻击者设置任意属性,包括不应由他们操作的管理员标记、角色和关联。
# WRONG — mass assignment vulnerability
User.create(params[:user])
@user.update(params[:user])
User.new(params.permit!) # permit! allows everything
# CORRECT — strong parameters
def user_params
params.require(:user).permit(:name, :email, :bio)
end
@user.update(user_params)
强参数指南:
- 在每个控制器中定义一个私有的
*_params方法 - 只允许用户应当能够设置的属性
- 绝不要允许用户输入
:role、:admin、:is_admin、:verified或类似的特权属性——权限提升正是由此发生的 - 嵌套属性需要显式允许
def post_params
params.require(:post).permit(
:title, :body, :published,
tags: [],
images_attributes: [:id, :url, :caption, :_destroy]
)
end
第 6 步:身份验证
无密码邮箱验证码身份验证(Basecamp、Fizzy、Herald): 不要使用 Rails 8 密码生成器。遵循 magic-link-auth 技能——验证码供人使用,会话/待处理/访问令牌供机器使用。
基于密码的应用: 从 Rails 8+ 身份验证生成器开始:
bin/rails generate authentication
身份验证关键实践:
- 使用
has_secure_password——它能正确处理 bcrypt 哈希 - 使用
authenticate_by——它具备时序安全性,可防止攻击者通过测量响应时间来枚举有效账户 - 登录后调用
reset_session,以防止会话固定(攻击者预先设置会话 ID) - 更改密码/邮箱时要求提供当前密码
- 使用通用错误消息:“邮箱或密码无效”(而不是“未找到用户”)——具体消息会泄露哪些账户存在
# Session creation — correct pattern
class SessionsController < ApplicationController
def create
if user = User.authenticate_by(
email_address: params[:email_address],
password: params[:password]
)
reset_session # Prevent session fixation
start_new_session_for user
redirect_to root_path
else
redirect_to new_session_path, alert: "Invalid email or password."
end
end
end
限制登录尝试频率:
class SessionsController < ApplicationController
rate_limit to: 10, within: 3.minutes, only: :create
end
第 7 步:授权
将查询限制在当前用户的范围内。 如果不限制范围,任何经过身份验证的用户都可以仅通过更改 URL 中的 ID 来访问任何记录。
# WRONG — any user can access any project by changing the ID
@project = Project.find(params[:id])
# CORRECT — scope to current user
@project = Current.user.projects.find(params[:id])
# CORRECT — use policy objects (Pundit, Action Policy, etc.)
@project = authorize Project.find(params[:id])
授权检查清单:
- 将每个查询限制在经过身份验证的用户权限范围内
- 在每个操作中检查授权(before_action)
- 对所有受保护的资源使用
before_action :authenticate - 优先采用默认拒绝:在 ApplicationController 中使用
before_action,并有选择地使用skip_before_action,这比选择性启用only:更安全(新操作默认受到保护,而不是意外暴露)
class ApplicationController < ActionController::Base
before_action :require_authentication
# All controllers require auth unless explicitly skipped
end
class PublicPagesController < ApplicationController
skip_before_action :require_authentication, only: [:home, :about]
end
第 8 步:会话安全
# config/environments/production.rb
config.force_ssl = true # HSTS + secure cookies + HTTPS redirect
# Session cookie settings (Rails defaults are good, but verify)
Rails.application.config.session_store :cookie_store,
key: "_myapp_session",
secure: Rails.env.production?,
httponly: true,
same_site: :lax
会话指南:
- 会话中只存储 ID——即使 Cookie 已签名,其中的敏感数据仍可读取
- 登录后调用
reset_session(防止会话固定) - 实现会话过期机制(绝对时限 + 空闲超时)
- 在生产环境中设置
force_ssl = true——否则会话 Cookie 会通过纯 HTTP 传输,在公共网络上可能被窃取
第 9 步:机密信息和凭据
使用 Rails 凭据或环境变量存储机密信息。 硬编码的机密信息最终会进入版本控制,任何能访问仓库的人(或获得泄露备份的人)都能拿到你的 API 密钥。
# Edit credentials
EDITOR=vim bin/rails credentials:edit
# Environment-specific credentials
EDITOR=vim bin/rails credentials:edit --environment production
# Access credentials
Rails.application.credentials.secret_key_base
Rails.application.credentials.dig(:aws, :access_key_id)
# Use bang version to fail loud if missing
Rails.application.credentials.some_api_key!
密钥管理:
- 绝不要提交
config/master.key或config/credentials/*.key——它们可以解密你的所有机密信息 - 将它们添加到
.gitignore(Rails 默认会这样做,但请确认) - 在生产环境中使用
ENV["RAILS_MASTER_KEY"] - 避免在日志中记录凭据,或在错误消息中暴露凭据
- 如果机密信息可能已泄露,立即轮换
第 10 步:内容安全策略
# 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
policy.connect_src :self, :https
policy.frame_ancestors :none
policy.report_uri "/csp-violation-report-endpoint"
end
# Use nonces instead of 'unsafe-inline'
Rails.application.config.content_security_policy_nonce_generator =
->(request) { SecureRandom.base64(16) }
Rails.application.config.content_security_policy_nonce_directives = %w[script-src]
布局必须包含:
<head>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= javascript_include_tag "application", nonce: true %>
<%= stylesheet_link_tag "application", nonce: true %>
</head>
第 11 步:日志安全
# config/initializers/filter_parameter_logging.rb
Rails.application.config.filter_parameters += [
:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp,
:ssn, :credit_card, :card_number, :cvv, :authorization
]
避免记录以下内容(它们会出现在日志文件、错误追踪工具以及可能的第三方服务中):
- 密码、令牌、API 密钥、机密信息
- 信用卡号、SSN、PII
- 可能包含敏感数据的完整请求正文
第 12 步:重定向安全
# WRONG — open redirect vulnerability
redirect_to params[:return_to]
# CORRECT — only allow internal paths
redirect_to safe_redirect_path(params[:return_to])
# Helper implementation
def safe_redirect_path(path)
uri = URI.parse(path.to_s)
if uri.relative? && uri.path.start_with?("/")
uri.path
else
root_path
end
rescue URI::InvalidURIError
root_path
end
# SIMPLEST — use url_from (Rails 7+)
redirect_to url_from(params[:return_to]) || root_path
第 13 步:文件上传安全
# Validate content type
class Document < ApplicationRecord
has_one_attached :file
validates :file, content_type: %w[application/pdf image/png image/jpeg],
size: { less_than: 10.megabytes }
end
# NEVER serve uploads from your app domain in production
# Use a separate domain/CDN or Active Storage's redirect mode
# NEVER use user-provided filenames for storage
# Active Storage handles this — it uses random keys
文件上传指南:
- 验证内容类型和文件扩展名——攻击者可以伪造其中一项,但很少能同时伪造两项
- 设置最大文件大小限制,防止通过大型上传造成拒绝服务
- 将上传内容存储在 Web 根目录之外(Active Storage 会自动处理)
- 切勿执行上传的文件
- 异步处理图像/媒体——图像处理库可能存在漏洞
- 在敏感应用中扫描恶意软件
快速参考
使用 Grep 搜索危险模式
# XSS: rg "\.html_safe|raw\(|<%==" app/
# SQL injection: rg '\.where\(".*#\{' app/ --type ruby
# Order injection: rg '\.order\(params' app/ --type ruby
# Mass assignment: rg 'params\.permit!' app/controllers/
# Open redirects: rg 'redirect_to.*params' app/controllers/
# CSRF disabled: rg 'skip_forgery_protection' app/controllers/
# Hardcoded secrets: rg '(api_key|secret|password|token)\s*=' app/ config/ -i
# Command injection: rg 'system\(|exec\(|`.*#\{' app/ --type ruby
正则表达式陷阱
# WRONG — ^ and $ match LINE boundaries in Ruby, not string boundaries
validates :url, format: { with: /^https?:\/\/.+$/ }
# CORRECT — use \A and \z for string boundaries
validates :url, format: { with: /\Ahttps?:\/\/.+\z/ }
反模式 — 以下每一项都是安全漏洞
- 对用户输入使用
html_safe— 会立即导致 XSS。攻击者注入的脚本会在其他用户的浏览器中运行 - 在 SQL 中进行字符串插值 — SQL 注入会让攻击者读取、修改或删除整个数据库
params.permit!— 允许所有参数,使攻击者能够设置管理员标记、角色或任何属性- 在浏览器控制器中使用
skip_forgery_protection— 恶意网站可以代表你的用户提交表单 redirect_to params[:url]— 开放重定向。攻击者可以制作看似来自你的网站、实际却会重定向到钓鱼页面的链接- 在源代码中硬编码密钥 — 任何有仓库访问权限的人(或拿到泄露备份的人)都能获取你的 API 密钥
- 使用带有用户输入的
Kernel#open—open("| rm -rf /")会执行 Shell 命令。请改用File.open、URI.open或IO.open - 在生产环境中禁用
force_ssl— 会话 Cookie 会以明文传输,在任何共享网络上都可能被窃取 - 记录敏感数据 — 日志会进入错误跟踪工具、日志聚合器和开发者的机器
- 信任客户端验证 — 攻击者可以轻易绕过 JavaScript;真正的关卡是服务端验证
安全审计清单
发布任何功能之前,请确认:
- 已启用 CSRF 保护(未跳过)
- 每个控制器操作都使用了强参数
- 用户提供的内容未使用
.html_safe或raw() - 所有 SQL 查询都使用参数化查询
- 查询范围限定为当前用户(授权)
- 已验证重定向目标(无开放重定向)
- 已从日志中过滤敏感参数
- 已验证文件上传(类型、大小)
- 源代码中没有硬编码的密钥
- 生产环境中设置了
force_ssl = true - 已配置 CSP 标头
- 身份验证使用了时间安全比较
- 登录后已重置会话
- 登录和敏感端点已启用速率限制