安全性權限修補 - 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)。
目標:
- 立即修補此漏洞(CVE 7.5 High)
- 檢查全專案是否有同類漏洞
- 建立自動化檢查(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 幫你系統化地從症狀爬到根因。