mcpField
S
spree-security

spree-security

github.com/spree安全10225 次安装4 周前更新
代码仓库

关于 spree-security

面向 Spree 6 扩展开发者的安全技能,涵盖密钥、API 访问、员工会话及面向顾客的接口,并介绍降低凭据、支付和跨店数据风险的实用控制措施。

主要功能

  1. 01密钥与加密密钥管理
  2. 02API 密钥范围控制
  3. 03员工会话保护
  4. 04按店铺限制记录访问
  5. 05质量赋值控制

适用场景

  1. 01为客户及网关数据配置加密密钥
  2. 02用 API 密钥范围限制集成权限
  3. 03检查自定义控制器是否暴露跨店数据
  4. 04设置 API 资源可写属性

安装

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

$npx skills add https://github.com/spree/agent-skills --skill spree-security

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

详细介绍

Spree 安全

Spree 沿用 Rails 的安全模型,并增加了电子商务攻击面:支付流程、客户个人身份信息、员工凭据和出站 Webhook。本技能涵盖开发者扩展 Spree 6 时必须妥善处理的事项。有关谁可以做什么(角色、权限键、作用域、店面所有权、SSO),请参阅 spree-auth-permissions。

四行威胁模型

  1. Store API:面向互联网,由不受信任的客户端调用。风险:购物车/订单上的 IDOR、通过商品目录内容造成的 XSS、滥用购物车/身份验证端点。
  2. Admin API + 仪表板:员工凭据可能遭到钓鱼,集成可能泄露密钥。风险:权限范围过大的密钥、自定义控制器中的跨店泄露、角色权限不断扩大。
  3. 支付:涉及资金和 PCI。风险:卡数据进入日志或数据库、网关 Webhook 未经验证、退款滥用。
  4. 出站流量:Webhook 和集成。风险:SSRF、接收方未经验证。

密钥

  • 将它们保存在 Rails 加密凭据或环境变量中,绝不要放进代码库。VITE_* 变量会编译进仪表板资源包,因此绝不要在其中放入密钥。
  • **密钥泄露:**先在提供商处轮换,然后更新凭据/环境变量并部署,接着清理 Git 历史记录(git filter-repo)。如果先清理历史记录,泄露的密钥在轮换前仍然有效。
  • **每个环境中的 secret_key_base 都必须保持稳定。**机密 API 密钥会使用它作为密钥生成 HMAC-SHA256 摘要,因此轮换它会使所有 sk_ 密钥失效。它也是 JWT 签名的最后备用密钥。使用 SPREE_JWT_SECRET_KEY(或凭据中的 jwt_secret_key)设置专用 JWT 密钥。
  • **Active Record 加密。**Spree 仅在配置了密钥时才会加密 Spree::WebhookEndpoint#secret_key 和 Spree::GatewayCustomer#profile_id(确定性加密),以及 Spree::UserIdentity#access_token/#refresh_token(OAuth 令牌);如果未配置,这些值将以明文存储,初始模板会在启动时记录警告。设置以下三个环境变量:ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY、ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY、ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT。
    • create-spree-app 会将一组开发环境密钥写入 .env;spree encryption init 会为较早项目的 .env 添加密钥(绝不覆盖现有密钥,也没有 --force;之后使用 spree update 重新创建容器,若已弹出则使用 spree dev——spree restart 会保留旧环境变量);spree encryption init --print 或 bin/rails db:encryption:init 会打印一组新的生产环境密钥。每个环境使用单独的一组密钥,并将其备份到密钥管理器中。
    • 初始模板的 config/application.rb 会将环境变量(若无则回退到 active_record_encryption 凭据项)复制到 config.active_record.encryption 中。这很重要:webhook 密钥和网关客户的 encrypts 调用会检查 Rails.configuration.active_record.encryption,因此,如果密钥只存在于凭据中而缺少这段代码,这两列就会以明文存储。使用较早初始模板创建的应用需要添加这段代码(参见 spree-upgrade-5-to-6)。
    • **数据加密后,绝不要更改或丢失密钥。**Rails 可以轮换主密钥,但无法轮换 webhook 密钥和网关客户 ID 使用的确定性密钥/盐值。
    • 为现有数据启用加密:身份令牌仍可读取(support_unencrypted_data: true),并会在下次写入时加密。但 webhook 密钥和网关客户 ID 不会这样处理——设置 config.active_record.encryption.support_unencrypted_data = true 和 extend_queries = true,携带密钥部署,运行 [Spree::WebhookEndpoint, Spree::GatewayCustomer, Spree::UserIdentity].each { |m| m.find_each(&:encrypt) },然后移除这两项设置。
  • **支付方式和集成偏好设置未加密。**网关凭据存储在序列化的 preferences 列中,因此请将数据库及其备份视为存有有效密钥。通过仪表板(Settings → Payments)输入有效的网关密钥,不要将其写入种子数据。

spree/agent-skills 插件附带一个钩子,可在 Agent 写入符合已知格式的密钥(Stripe 有效密钥、AWS 密钥、PAT)时发出警告。它只是预警机制,不能替代审查。

API 密钥

pk_*  Publishable. Safe in browser and mobile code. Store API only.
sk_*  Secret. Server-to-server only. Never ship it in a bundle, app or VITE_* variable.
  • **最低限度的权限范围。**使用 spree api-key create --type secret --scopes read_orders,write_fulfillments。不要向集成授予 write_all。权限范围列表见 spree-api-v3/references/scopes.md。
  • 权限范围和渠道绑定均不可更改。如需轮换或更改访问权限,请创建新密钥、部署新密钥,然后撤销旧密钥(Settings → API keys,或使用 spree api-key revoke)。
  • 使用机密密钥进行的写入操作会归属到该密钥(*_type: api_key),因此你可以通过 actor 字段和 WebhookDelivery 日志审计泄露密钥造成的影响。
  • 将店面的可发布密钥绑定到其渠道,这样购物者就无法通过发送 X-Spree-Channel 切换渠道。
  • 泄露 pk_ 只是麻烦:限制速率并轮换。泄露 sk_ 则是安全漏洞:撤销密钥,然后进行审计。

仪表板与员工会话

React 仪表板(@spree/dashboard,由 spree_dashboard 在 /dashboard 提供)不保存 API 密钥:

  • 员工登录后,JWT 仅保存在内存中(默认有效期为 5 分钟,由 admin_jwt_expiration 配置)。刷新令牌是一个作用域限定为 /api/v3/admin/auth 的 HttpOnly 签名 Cookie。JavaScript 无法读取它,而且每次刷新时都会轮换。
  • 使用 HTTPS 时,Cookie 的属性为 SameSite=None; Secure。使用纯 HTTP 时,会回退为 SameSite=Lax(仅限开发环境)。
  • 同源(默认设置:Rails 在 /dashboard 提供仪表板)无需配置 CORS 或 Cookie。
  • 跨源(仪表板托管在 CDN 上):两端都必须使用 HTTPS,并且在 Settings → Allowed Origins(Spree::AllowedOrigin,/api/v3/admin/allowed_origins)中添加仪表板来源。起始模板中的 config/initializers/cors.rb 会针对 /api/v3/admin/* 查询该表,并设置 credentials: true。Cookie 的 CSRF 防护依赖 SameSite 和严格的来源允许列表,因此列表应准确且简短。
  • UI 中的限制不等于授权。 仪表板根据 GET /api/v3/admin/me 返回的权限键隐藏按钮,但 API 端的授权关卡才负责强制执行权限。任何仪表板插件操作都必须调用声明了 scoped_resource 的端点(参见 spree-auth-permissions)。该关卡之后的记录级拒绝由 Spree.ability_class 决定——自定义子类可以进一步限制员工访问,但绝不会放宽权限键关卡;密钥(Spree::ApiKeyAbility)则绕过该关卡。
  • 员工 SSO:注册 OidcStrategy,并可选择从 Spree.admin_authentication_strategies 中移除 :email。绝不会根据 IdP 自动创建员工账号。
  • 员工管理内置了防护措施(详见 spree-auth-permissions):只有管理员可以授予或移除 admin 角色,且每个商店至少保留一名管理员;邀请必须指定角色;邀请接受链接来自单独的写入权限受控端点(绝不会来自列表),重新发送邀请会轮换令牌;员工密码重置链接只会指向仪表板来源;登录、当前密码确认和接受邀请共用一个锁定机制(Spree::Authentication::Lockout)——你添加的任何密码检查也应通过该机制进行。
  • 为每位员工创建独立账号,并分配最小权限角色。不要共用 admin@ 登录账号。
Mission Control(/jobs)

作业界面受 HTTP Basic 认证保护。生产环境中请设置 MISSION_CONTROL_USER 和 MISSION_CONTROL_PASSWORD;未设置时,仪表板会保持锁定。起始模板的本地默认值(spree / spree123)仅适用于开发和测试环境。切勿将它们复制到生产环境配置文件中。

Store API:IDOR 与跨商店数据泄漏

  • 购物车和订单可由其所有者(JWT)或持有访客令牌(X-Spree-Token)的人读取。任何持有该令牌的人都能读取对应的购物车或订单,因此绝不要记录令牌,也不要将令牌放入要分享的 URL 中。
  • 带前缀的 ID 不是秘密(它们是顺序键的编码)。每次查找都必须进行授权;绝不要依赖 ID 难以猜测。
  • 在自定义 Store 控制器中,通过 storefront_access_policy.scope(Model.for_store(current_store)) 或 current_user.<association> 读取数据。绝不要使用 Model.find(params[:id])。
  • 在自定义 Admin 控制器中,使用继承的 scope(按商店限定范围)并声明 scoped_resource。多商店应用共用一个数据库,因此在控制器中调用 Spree::Order.find 会造成跨商店数据泄漏。
  • 写入请求体中的每个 ID 也都是一次查找。 通过 current_store(在卖家端接口上还要通过卖家)解析引用的 ID(stock_location_id、reason_id、自定义字段的父记录、翻译目标、价格列表中的变体、促销规则中的商品),并从路由中获取父记录,绝不要从查询参数或请求体参数中获取。核心代码对自身端点会执行这些操作;自定义控制器若直接将参数中的 *_id 赋值给记录,就会再次造成跨商店数据泄漏。
  • 在开发和测试环境中,Spree::StoreScopeGuard 会标记对商店所有表(任何包含 store_id 的 spree_* 表)执行的 SELECT,前提是这些查询既未按商店限定范围,也未按 ID 筛选。它会监视每个 API v3 请求,以及任何设置 Spree::Current.store 的工作单元——包括任务、webhook 控制器、控制台脚本和规格测试——直到 Spree::Current 重置。模式:SPREE_STORE_SCOPE_GUARD / Spree::Config[:store_scope_guard] = log(默认)、raise(Spree 自身的 API 测试套件)、off;在生产环境中绝不会启用。使用 Spree::StoreScopeGuard.skip { … } 包裹有意执行的全局查询。请认真对待它的警告。

批量赋值

API v3 控制器根据 resource_permitted_attributes(自定义控制器)和各模型的 additional_permitted_attributes 构建允许列表。不存在 Spree::PermittedAttributes 模块。不要覆盖 permitted_params——这会丢弃扩展添加的内容。

# Make an extension column writable on the existing endpoints (never `<<`, the array is frozen):
Spree::Product.additional_permitted_attributes += [:brand_id]
Spree::Address.additional_permitted_attributes += [{ tag_ids: [] }]

# Custom controllers list their own:
def resource_permitted_attributes = %i[name rating body]

绝不要使用 params.permit!。绝不要在面向客户的端点中允许所有权或权限列(customer_id、store_id、role_ids、status)。请在服务器端设置这些值。

注入和 XSS

  • **SQL:**使用参数化的 where('x > ?', v) 或哈希条件,绝不使用字符串插值。Ransack 是安全的,因为它只会筛选允许列表中的属性。未知的 q[...] 谓词会被静默丢弃。使用 Spree.ransack.add_attribute(Model, :attr) 暴露新筛选条件,绝不要将机密或摘要列入允许列表。筛选条件相当于是/否预言机,因此 Store 和 Seller API 的允许列表比员工端更严格(storefront_ransackable_associations、以 :store/:seller 为键的 private_ransackable_attributes/private_ransackable_scopes);扩大店面允许列表时,要确保私有数据不在其中。继承 Store/Seller ResourceController 的控制器会自动传入该参数(ransack_auth_object);面向客户的控制器中手动编写的查询则应自行调用 ransack(params, auth_object: :store)。
  • 富文本:商品、类别、集合和卖家描述会在保存时由 Spree::RichTextSanitizer 清理(通过 has_spree_rich_text / sanitizes_rich_text)。允许列表涵盖仪表板 Tiptap 编辑器生成的内容:p br hr h1–h6 strong em s u code pre blockquote ul ol li a img,并会移除 data:/javascript: URL。API 将 description 作为纯文本返回,将 description_html 作为已清理的标记返回。
    • 如需有意扩大允许列表:在初始化器中使用 Spree::RichTextSanitizer.allowed_tags += %w[table thead tbody tr th td]。
    • 跳过回调的写入(update_columns、update_all、原始 SQL、绕过模型的批量导入)不会经过清理。请自行调用 Spree::RichTextSanitizer.sanitize(html)。
    • 在模型中新增富文本列:include Spree::SanitizableRichText + has_spree_rich_text :body。翻译属性也需要在 Translation 类上使用 sanitizes_rich_text。
  • **店面:**仅对这些经服务器清理的字段使用框架提供的原始 HTML 转义入口(例如 React 的 dangerouslySetInnerHTML)来渲染 *_html。其他所有内容都要转义。
  • **CSV 导出:**写入供员工用电子表格打开的文件前,应将以 =、+、- 或 @ 开头的单元格内容处理为安全值。

CORS 和 CSP

# config/initializers/cors.rb: storefront on another origin (the admin block comes from the starter)
allow do
  origins 'https://shop.example.com'
  resource '/api/v3/store/*', headers: :any, methods: %i[get post patch put delete options]
end
  • 绝不要同时使用 origins '*' 和 credentials: true。管理端和跨域卖家面板必须使用 Allowed Origins 表。
  • 在渲染 HTML 的应用上设置 CSP。对于 Next.js 店面,这意味着要在 Next 应用上设置,而不是 Rails。Rails 端主要提供 JSON 和仪表板。

Webhooks

出站(Spree → 你的接收端)。每次投递都会携带 X-Spree-Webhook-Signature = 使用端点密钥对 "#{timestamp}.#{raw_body}" 计算的十六进制 HMAC-SHA256,以及 X-Spree-Webhook-Timestamp 和 X-Spree-Webhook-Event。接收端必须:

  1. 在解析 JSON 之前,使用原始请求体字节进行验证。
  2. 使用恒定时间函数进行比较(ActiveSupport::SecurityUtils.secure_compare、crypto.timingSafeEqual)。
  3. 拒绝时间戳早于约 5 分钟的请求(防止重放)。
  4. 保证幂等。Spree::WebhookDeliveryJob 会在非 2xx 响应、超时和连接错误时通过退避机制重试(总共 5 次,事件 id 不变);手动重新投递也会再次发送同一事件。

@spree/sdk/webhooks 导出 verifyWebhookSignature(rawBody, signature, timestamp, secret, tolerance = 300)。请参阅 spree-events-webhooks。

SSRF。 除开发环境外,投递会经过 SsrfFilter,该过滤器会阻止指向私有、环回和链路本地地址的请求。开发环境会绕过该过滤器,因此接收端可以使用 localhost。不要将 Rails.env.development? 分支复制到其他代码路径。除开发环境外,webhooks_verify_ssl 默认开启;请保持开启。创建 Webhook 端点需要 write_webhooks,该权限与 settings 有意分开。

write_webhooks/read_webhooks 不授予的权限。 customer.password_reset_requested 携带有效的重置令牌:它只会发送到明确订阅该事件的端点(绝不会发送到订阅 * 或 customer.* 的端点);订阅该事件,或更改接收该事件的端点地址,也需要 write_customers。投递日志会将凭据(购物车 token、重置/验证令牌、下载 URL、支付会话密钥、礼品卡 code)存储为 [REDACTED];Admin API 只会向有权读取底层记录的调用方返回投递的 payload(否则为 null)。

入站(网关 → Spree,例如 /api/v3/webhooks/payments/...):仅凭带前缀的 ID 查找记录(请求无法指定商店),请求会在对应支付方式/集成所属的商店中运行,提供商会使用该记录自身的密钥验证签名——无效时返回 401。签名是唯一的身份验证方式。如果你编写自定义提供商,请在执行操作前验证签名(参阅 spree-providers、spree-payments)。

数据隐私(GDPR)

  • 数据主体请求是 Spree::DataRequest 记录。客户通过 POST /api/v3/store/customers/me/data_requests { kind: "access" | "erasure" } 创建请求(擦除需要 current_password)。员工使用 GET /api/v3/admin/customers/:id/export 和 POST /api/v3/admin/customers/:id/anonymize。导出在后台生成,并通过电子邮件发送带有效期的签名链接。
  • 擦除意味着匿名化,由 Spree::Customers::Anonymize 工作流执行。该流程会清理账户、地址簿、订单地址快照、已保存的卡、身份信息和会话,并从同意记录中移除电子邮件、IP 和用户代理(记录本身会保留——参见下文“同意”)。财务记录会保留(订单、付款、税费明细、商品项,以及用于税务管辖的国家、州和截断后的邮政编码),并发布 customer.anonymized。
  • 如果你添加了存储个人数据的表,必须在同一次变更中扩展匿名化处理。 Core 中有一个架构保护规范;如果某个个人数据列未纳入处理范围,该规范就会失败。
  • 钩子:
    Rails.application.config.after_initialize do
      Spree.hooks.register('customers.anonymize.validate', 'MyApp::LegalHold')            # workflow.reject!('Under legal hold')
      Spree.hooks.register('data_requests.fulfill.extend_payload', 'MyApp::LoyaltyExport') # return a Hash to merge
    end
    
  • 同意:Spree::ConsentRecord 记录接受事件(目的、来源、时间、文档摘要)。客户还具有 email_marketing_consent_updated_at / _source。擦除后,同意记录会保留(目的、来源和时间戳作为证明),但电子邮件、IP 和用户代理会被清除;如果客户曾选择加入营销,还会追加一条 email_marketing 撤回记录(来源为 anonymization)。Cookie 同意由店面负责处理。
  • 员工通过 read_customers / write_customers 访问 DataRequest 和 ConsentRecord。

PCI 范围

Spree 从不存储或传输 PAN。银行卡数据通过支付网关托管字段,从浏览器直接发送到支付网关(通过 spree_stripe 使用 Stripe Elements / Payment Element、Adyen Drop-in、PayPal)。Spree 只接收令牌和支付会话。Spree::CreditCard 保存品牌、末 4 位、有效期和支付网关引用,从不保存完整卡号或 CVC。

  • 绝不要添加银行卡数据列,也不要通过你的 API 代理原始银行卡字段。如果你认为有此需要,请改用支付网关令牌化。
  • 仅使用托管字段时,通常适用 SAQ A。自行收集银行卡数据则适用 SAQ D。
  • 参数过滤已涵盖 number、verification_value 等参数。对于自定义敏感参数名,请扩展 Rails.application.config.filter_parameters。

速率限制

API 内置了限流:按可发布密钥 + IP 限制(300 次/分钟)、按密钥限制(600 次/分钟),并对登录、注册、刷新和密码重置按 IP 限制。计数器存储在 Rails.cache 中,因此多进程部署需要共享存储(Solid Cache 或 Redis)。在前方部署 CDN 或 WAF,以防范流量型滥用。参阅 spree-api-v3 中的表格。

依赖项卫生

在 CI 中对店面和仪表板运行 bundle audit、brakeman 和 pnpm audit。此插件不提供 CI 工作流。

部署清单

  • 在 credentials/env 中设置密钥。不要将密钥放在 VITE_*、代码仓库或种子数据中。
  • 确保 secret_key_base 稳定;设置 SPREE_JWT_SECRET_KEY;设置三个 ACTIVE_RECORD_ENCRYPTION_* 密钥(仅限生产环境,且应备份),并将其读入 config.active_record.encryption。
  • 设置 config.force_ssl = true;API、仪表板和店面均启用 HTTPS。
  • Allowed Origins 仅列出实际的仪表板/卖家面板来源。店面 CORS 显式列出允许的来源。
  • 设置 MISSION_CONTROL_USER / MISSION_CONTROL_PASSWORD。
  • 集成使用权限范围最小的密钥,并根据用途为每个密钥命名。write_all 仅用于紧急情况。
  • 员工均使用个人账户和最低权限角色。如果组织有 IdP,则强制使用 SSO。
  • Webhook 接收端验证 HMAC、检查时间戳,并确保操作具备幂等性。
  • 自定义控制器限定到店铺范围(Admin)或所有权范围(Store)。
  • 匿名化处理涵盖你添加的所有个人数据表。
  • 为速率限制使用共享缓存存储。在前方部署 CDN/WAF。
  • 加密数据库备份,并存储在远离数据库的位置。

深入阅读

  • PCI:node_modules/@spree/docs/dist/developer/security/pci_compliance.md
  • 数据隐私:node_modules/@spree/docs/dist/developer/core-concepts/data-privacy.md
  • 仪表板部署与身份验证:node_modules/@spree/docs/dist/developer/dashboard/deployment.md
  • Admin API 身份验证:node_modules/@spree/docs/dist/api-reference/admin-api/authentication.md
  • 后台作业 / Mission Control:node_modules/@spree/docs/dist/developer/deployment/background_jobs.md
  • Rails 安全指南:https://guides.rubyonrails.org/security.html
  • 相关技能:spree-auth-permissions、spree-api-v3、spree-events-webhooks、spree-payments、spree-deployment。

评论

支持 Markdown