TTechSpecs

安全性權限修補 - Security Authorization Patch 實戰範例

外部資安公司寄來滲透測試報告,白紙黑字寫著:「可透過修改 URL 中的 user_id 存取他人帳單資料」。 這是 OWASP Top 10 中的 Broken Access Control(漏洞 #1),業界俗稱 IDOR(Insecure Direct Object Reference)

本範例示範如何用 Superpowers 的除錯與驗證 skills:不僅修掉這一個漏洞,還建立組織級的防護網防止同類問題再生

專案概述

情境:Ruby on Rails 電商平台。InvoicesController#show 直接用 params[:id] 查詢發票,未檢查是否屬於 current_user。資安公司證實:登入 A 帳號後 GET /invoices/42 可看到使用者 B 的發票(只要猜 ID)。

目標:

  1. 立即修補此漏洞(CVE 7.5 High)
  2. 檢查全專案是否有同類漏洞
  3. 建立自動化檢查(Pundit policy + controller-level test)避免再發生

技術棧:Ruby 3.2 + Rails 7 + Pundit + RSpec

核心挑戰:

  • 發票總數 ~120 萬筆(連續 ID 好 guess)
  • 同類別 controller 有 14 個(orders、addresses、.subscriptions、payment_methods…)
  • 老程式碼多處直接用 Model.find(params[:id])
  • 既有 controller test 覆蓋率僅 40%

完整開發流程

第 1 步:Systematic Debugging 找根因(不只找症狀)

鐵律:不做根因調查不許提修復。看到報告不要急著改 InvoicesController

第 1 階段:穩定復現

# spec/requests/security/invoices_idor_spec.rb (失敗的 security test)
RSpec.describe 'Invoices IDOR vulnerability' do
  it 'prevents user A from accessing user B invoices' do
    user_a = create(:user)
    user_b = create(:user)
    invoice_b = create(:invoice, user: user_b)

    sign_in user_a
    get invoice_path(invoice_b)  # 嘗試存取 B 的發票

    expect(response).not_to have_http_status(:ok)
    expect(response).to have_http_status(:not_found).or have_http_status(:forbidden)
  end
end

跑測試 → 復現成功(紅燈:user A 拿到 200 + 完整發票內容)。

第 2 階段:找斷點 — 不只 InvoicesController

向上追蹤發現 14 個 controller 都有相同模式:

# 全專案 grep 「直接用 params[:id] 查」反模式
$ grep -rn '\.find(params\[:id\])' app/controllers/
app/controllers/invoices_controller.rb:12:    @invoice = Invoice.find(params[:id])
app/controllers/orders_controller.rb:18:      @order = Order.find(params[:id])
app/controllers/addresses_controller.rb:9:    @address = Address.find(params[:id])
app/controllers/payment_methods_controller.rb:11:@payment_method = PaymentMethod.find(params[:id])
# ... 共 14 處

根因:缺統一的 authorization layer。修單一 controller 就是表面修補,下週還會在 OrdersController 被發現同樣漏洞。

第 2 步:Writing Plans 制定修補計畫

觸發:

使用 writing-plans skill 制定 IDOR 漏洞的根因修補計畫

計畫(三層防護):

階段 1(1 天):立即修補 + Hotfix 部署
- [ ] InvoicesController 改用 current_user.invoices.find
- [ ] 新增 security test(已在第 1 步)
- [ ] hotfix-2025-07 分支,主管核准,緊急上線

階段 2(2 天):全專案同類修補
- [ ] 14 個 controller 一致改用 scope-based 查詢
- [ ] 每個 controller 都加 security test(spec/requests/security/*)
- [ ] CI 加 rubocop-rspec 規則禁止 find(params[:id])

階段 3(3 天):建立防護網(治本)
- [ ] 引入 Pundit policy(User policy、Invoice policy...)
- [ ] before_action :authorize_resource!
- [ ] 全專案 policy 覆蓋率 100%
- [ ] 滲透測試公司 retest

驗收標準:
- [ ] 資安公司 retest 由 High 降到 0
- [ ] 14 個 controller 全部有 security test
- [ ] CI 強制 policy check,缺 policy 無法 merge

第 3 步:TDD 修補階段 1(立即修補)

紅燈:第 1 步的 security test 已備。綠燈:

# app/controllers/invoices_controller.rb (修復後)
class InvoicesController < ApplicationController
  def show
    @invoice = current_user.invoices.find(params[:id])
    # ↑ 改用 current_user 關聯 scope,自動過濾「非自己的」發票
    # 若 id 不屬於 current_user,ActiveRecord 拋 RecordNotFound → Rails 自動 404
  rescue ActiveRecord::RecordNotFound
    render file: Rails.public_path.join('404.html'), status: :not_found, layout: false
  end
end

跑測試 → 綠燈

注意:回應 404 而非 403。這是安全慣例 — 不透露「該資源存在但無權限」,讓攻擊者無法區分「沒這個 ID」與「沒權限」。

第 4 步:Executing Plans 階段 2 + 3(級聯修補)

把第 3 步的模式套用到其他 13 個 controller。為避免逐一粘貼,先抽 concern:

# app/controllers/concerns/owned_resource.rb
module OwnedResource
  extend ActiveSupport::Concern

  included do
    before_action :set_owned_resource, only: %i[show edit update destroy]
  end

  private

  def set_owned_resource
    # 各 controller 實作 resource_scope
    instance_variable_set(
      :"@#{controller_name.singularize}",
      resource_scope.find(params[:id])
    )
  rescue ActiveRecord::RecordNotFound
    head :not_found
  end

  def resource_scope
    raise NotImplementedError, "#{self.class} 必須實作 #resource_scope"
  end
end

# app/controllers/invoices_controller.rb (重構後)
class InvoicesController < ApplicationController
  include OwnedResource

  private

  def resource_scope
    current_user.invoices
  end
end

每個 controller 對應一個 security test。CI 加入自訂 Cop:

# .rubocop/custom/no_find_by_id.rb
class NoFindById < RuboCop::Cop::Base
  MSG = '禁止直接用 Model.find(params[:id]),請用 scope-based 查詢。'.freeze

  def on_send(node)
    return unless node.method_name == :find
    return unless node.arguments.any? { |a| a.source.include?('params[:id]') }
    add_offense(node)
  end
end

第 5 步:建立 Pundit 防護網(階段 3)

引入 Pundit 後,policy 成為單一真理來源:

# app/policies/invoice_policy.rb
class InvoicePolicy < ApplicationPolicy
  def show?
    record.user_id == user.id
  end

  def update?
    show? && !record.locked?
  end

  # ... 其他 action
end

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include Pundit::Authorization
  after_action :verify_authorized, except: :index  # ← 強制每個 action 都過 policy
  after_action :verify_policy_scoped, only: :index
  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

  private

  def user_not_authorized
    head :not_found  # 一樣用 404 隱藏存在性
  end
end

重點:after_action :verify_authorized 會在 production 丟錯,任何 action 沒寫 authorize @user 就無法上線。這是制度化的強制防護網。

第 6 步:Verification 驗收

觸發:

使用 verification-before-completion skill 驗證 IDOR 漏洞修補完整性

驗收清單:

✅ 漏洞修復
- 原漏洞 security test 反轉為紅→綠並 commit 為回歸 ✓
- 資安公司 retest 由 High → 0 Critical/High ✓

✅ 同類問題清除
- 14 個 controller 全數套用 OwnedResource concern ✓
- 14 個對應 security test 全綠 ✓

✅ 防護網運作
- Pundit policy 覆蓋率 100%(每個 action 都有 policy)✓
- after_action :verify_authorized 在 production 強制啟用 ✓
- RuboCop 自訂 Cop 在 CI 攔截 find(params[:id]) ✓
- 嘗試在 PR 改回 find(params[:id]) 被自動 block ✓

✅ 流程落地
- OWASP Top 10 訓練給後端團隊(20 人,2 小時)✓
- 新人 onboarding 文件新增「Authorization layer 必過」條款 ✓

關鍵學習點

1. 安全修補要治本,不是治症狀

資安報告的每一條都是「症狀」。背後的根因通常是「缺統一的 authorization layer」「缺自動化檢查」「缺測試」。只補單一 controller 等於在沙灘上蓋城堡——下個 PR 還會帶回同樣漏洞。

2. Security Test 應該寫在「攻擊者視角」

不要寫「get invoice_path 應回 200」這種 happy-path 測試。要寫「身分 A 嘗試存取身分 B 的資源,應被擋」。後者叫 negative authorization test, OWASP 建議覆蓋率 ≥ 100% public action。

3. 制度化優於道德勸說

“記得加 policy” 這種文案用說的不會有人記得。把檢查塞進 CI、用 after_action :verify_authorized 在 runtime 強制、用自訂 RuboCop 攔截,讓犯錯變成不可能,才是長期治本。

4. 回 404 還是 403?

公布的慣例是 404:不讓攻擊者透過 status code 區分「ID 存在 + 無權限」與「ID 不存在」。雖然放棄了一點 debugging 易用性,但在 security 第一的 endpoint 是值回票價的取捨。

工具使用摘要

Skill 用途 在本例的作用
Systematic Debugging 根因調查 從單一漏洞追溯到 14 處同類問題
Writing Plans 拆解計畫 立即修補 → 級聯修補 → 制度化三層
Test-Driven Development 反轉漏洞為 test 紅燈復現 → 綠燈修補 → 永久回歸
Executing Plans 執行級聯 concern 抽象 + 14 controller 套用
Verification 完成前驗收 四面向(漏洞、同類、防護網、流程)

總結

IDOR 看似簡單(把 find 改為 current_user.invoices.find),但 superpowers 的價值在於把單一修補升級成組織級防護:

  • Systematic Debugging 強迫你問「還有哪裡有同樣 bug」
  • Writing Plans 把工作分成「立即/級聯/治本」三層,不會只做第一層就收工
  • TDD 把漏洞寫成永久回歸測試
  • 制度化(verify_authorized + RuboCop + policy 100%)讓同類問題無法再生

下次再收到資安報告,記得:報告列的是症狀,根因永遠在層級更高處。Superpowers skills 幫你系統化地從症狀爬到根因。