mcpField
R
rails-security-multitenancy

rails-security-multitenancy

github.com/marckohlbrugge安全73059 次安装20 周前更新
代码仓库

关于 rails-security-multitenancy

面向安全敏感的 Rails 开发与多租户边界审查,整理了租户隔离、认证、授权和 SSRF 防护模式。内容以 Fizzy 和 Campfire 的实践为参考。

主要功能

  1. 01按租户与用户范围查询数据
  2. 02强化认证与请求限流
  3. 03防护用户提供的 URL
  4. 04隔离实时广播与会话
  5. 05覆盖 CSRF、缓存与授权

适用场景

  1. 01审查多租户 Rails 数据访问
  2. 02设计魔法链接与 API Token 认证
  3. 03保护 Webhook 和推送 URL
  4. 04检查租户间广播与存储授权

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/marckohlbrugge/37signals-skills --skill rails-security-multitenancy

安装后在 Claude Code 中输入/rails-security-multitenancy确认已加载。

详细介绍

Rails 安全与多租户

适用于对安全敏感的 Rails 工作,以及租户边界审查。模式来自 Fizzy(基于路径的多租户 SaaS)和 Campfire(单租户、机器人 API)。

核心规则

  • 所有租户数据访问都必须限定在租户/用户所有权边界内。
  • 在涉及租户的流程中,绝不要信任裸用 Model.find(params[:id]) 的方式。
  • 按租户/账户限定实时广播和流名称。
  • 对身份验证和易受滥用的端点实施速率限制。
  • 将用户提供的 URL 视为不可信输入。
  • 无法确认访问权限时,默认拒绝(head :forbidden)。

租户架构(基于路径,Fizzy 风格)

  • 中间件从 PATH_INFO 中提取账户前缀并写入 SCRIPT_NAME,加载账户,然后在 Current.with_account(account) 中处理请求。URL 助手会自动保持租户上下文正确(包括 ActiveStorage 和通过 script_name: 生成的 webhook 负载 URL)。
  • 三层身份验证:Identity → Session → 账户范围内的 User。 Session 关联到 Identity;每个请求都会解析 Current.user = identity.users.find_by(account: Current.account)。在账户 A 中有效的 Session 绝不能在账户 B 中执行操作。在 ActionCable 的 Connection#connect 中应用相同的解析逻辑。
  • Current 的 setter 会级联处理:赋值 session 时会解析 identity,赋值 identity 时会为当前账户解析 user。
  • 身份验证路由(登录、注册、魔法链接)会显式退出租户范围(disallow_account_scope),并将请求从带租户前缀的 URL 重定向出去。
  • 支持同时登录多个租户时,按路径范围限定会话 Cookie(path: account.slug)。
  • 定期任务在请求上下文之外运行:显式遍历租户(with_each_tenant)。将 Current.account 序列化到任务负载中(参见 rails-jobs)。
  • 使用 Rack::MockRequest 单独测试租户中间件;集成测试则根据测试夹具账户设置 default_url_options[:script_name]。

限定范围的查询(深度防御)

参数只能选择已获授权集合中的哪条记录,绝不能据此授予访问权限:

@card = Current.user.accessible_cards.find_by!(number: params[:card_id])   # access graph
@membership = Current.user.memberships.find_by!(room_id: params[:room_id]) # join model
@user = Current.account.users.find(params[:user_id])                       # tenant association
  • 即使是单租户代码,也要通过关联进行范围限定;错误的 ID 会自然返回 404。
  • 公开分享使用不透明令牌(在 Publication 记录上使用 has_secure_token :key),绝不使用内部 ID。
  • ActiveStorage:将 blob 关联到账户,并通过领域模型授权 blob/representation 控制器(blob → attachment → record.accessible_to?(user));已发布内容使用显式的 publicly_accessible? 路径。
  • 撤销访问权限时,通过范围受限的异步任务清理衍生数据(提及、通知、关注记录),不要留下跨边界的残留状态。

强化身份验证

  • 魔法链接/验证码:只能使用一次(使用后销毁记录)、有效期短、使用 secure_compare 比较,并通过已验证的待处理身份验证 Cookie 绑定到电子邮件。
  • 防止枚举:未知电子邮件地址也要获得与真实地址相同的模拟流程/用户体验。
  • API 令牌:按 HTTP 方法限定权限(只读令牌不能执行 POST);生成的密钥仅通过短期消息验证器(约 10 秒)展示一次,此后不再展示。
  • 将机器人/自动化身份验证设为显式模式:仅对机器人密钥身份验证跳过 CSRF;默认在所有位置拒绝机器人(deny_bots + allow_bot_access only:)。
  • 使用 Rails 内置功能限制速率,并确保响应符合端点语义:
rate_limit to: 10, within: 15.minutes, only: :create,
  with: -> { redirect_to ..., alert: "Try again in 15 minutes." }
  • 限制会话记录的写入频率(每小时最多更新一次 last_active_at)。
  • 除密码外,也要过滤敏感参数:消息正文、推送端点、令牌。

SSRF 防御基线

对于 webhook、推送端点、展开预览等任何受用户影响的 URL:

  • 请求前解析 DNS 并验证目标 IP;阻止回环、私有、链路本地和 IPv4 映射的 IPv6 地址范围(链路本地地址范围 = 云元数据)。
  • 将请求固定到已验证的 IP(Net::HTTP.new(host, port, ipaddr: resolved_ip)),以防 DNS 重绑定。
  • 创建时和执行时都要验证。
  • 每次重定向跳转时重新解析并重新验证——重定向链是经典的绕过方式。
  • 限制响应大小(预先检查 content-length,并限制分块读取大小),以防内存 DoS。
  • 在适用的情况下,叠加允许列表(已知目标集合时):Web Push 需要允许的供应商主机后缀,并解析为公共 IP。

CSRF 和缓存

  • 绝不对渲染表单或 CSRF 令牌的页面进行 HTTP 缓存。
  • 在来源检查的基础上添加 Sec-Fetch-Site 验证(same-origin/same-site);先以仅报告模式逐步启用,并将 Sec-Fetch-Site 添加到 Vary 标头中。
  • 设置具有严格底线的 CSP(object_src :none、base_uri :none、frame_ancestors :self);先使用仅报告模式和 report_uri 验证,再强制执行。
  • 私有应用:发送 X-Robots-Tag: none。

授权默认做法

  • 在模型上定义谓词方法(card.editable_by?(user)、user.can_administer_board?(board));控制器进行检查,并返回 head :forbidden。
  • 使用声明式控制器宏(allow_unauthenticated_access、ensure_can_administer),而不是散布条件判断。
  • 将共享的防护逻辑集中到小型 concern 中。

滥用应对

  • 封禁用户时进行级联处理:将其会话 IP 转换为封禁项,远程断开 ActionCable 连接,删除会话,并异步清除内容,同时向 UI 广播。
  • 仅对变更请求(POST/PUT/PATCH/DELETE)强制执行 IP 封禁。
  • 验证封禁目标为公共 IP——绝不允许封禁回环或私有地址范围。
  • 断开已停用用户的连接:ActionCable.server.remote_connections.where(current_user: user).disconnect(reconnect: false)。

入站 Webhook(接收)

  • 验证签名(例如 Stripe::Webhook.construct_event),然后从来源 API 重新获取规范状态,而不是信任负载内容或顺序。

危险信号

  • 未按租户限定范围的广播频道或流名称。
  • 未经所有权检查,便从请求参数推断租户数据。
  • 根据会话查找全局 User,却未按账户限定范围(混淆身份与用户)。
  • 向未经验证的目标发送 webhook 或获取请求;跟随重定向时未重新验证。
  • 绕过域授权的 ActiveStorage URL。
  • 安全控制隐藏在各控制器临时添加的条件判断中。
  • 管理员界面中永久可见的密钥。
  • 未进行速率限制的认证端点。

评论

支持 Markdown