iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
AI Security

我也希望安全第一——然後我們看看這些鎖是怎麼一個個被撬開的系列 第 16

第 16 篇|出錯之後為什麼救不回來——rollback、補償操作與不可逆行動

  • 分享至 

  • xImage
  •  

出錯之後為什麼救不回來——rollback、補償操作與不可逆行動

[[我也希望安全第一]]|第 16/30 天

「壞消息,我加不回去了」

一名澳洲使用者請 Claude Agent 幫忙預約候補中的健身課。Agent 找到訂課系統的漏洞,利用它把前面的人移出候補,讓使用者排到更前面。使用者發現後要求復原,Agent 回覆:壞消息,我加不回去了。

這件事規模不大,卻把 Agent 產品最常被忽略的一條線照得很清楚。資料庫裡的更新可以 rollback,不代表外部世界能 rollback。被踢掉的人可能已收到通知、錯過位置;第三方 API 也未必提供「把原順序完整放回去」的操作。

當副作用離開自己的 transaction boundary,undo 通常只是另一個動作,不是時間倒轉。

先替操作分可逆性

在 tool registry 裡加入 reversibility,比只標 readwrite 更接近真實風險:

類型 例子 失敗後能做什麼
純讀取 查詢訂單、讀 repo 停止後不需回復,但仍有隱私風險
可覆寫 更新自己資料庫的草稿 用舊 revision rollback
可補償 建訂單、發退款、預約 發出反向操作,但不保證完全還原
外部可見 寄信、發文、通知 可刪或更正,收件人可能已看見
不可逆 洩漏 secret、刪除無備份資料、占用他人名額 只能減害、通知與賠償

真正需要人類確認的往往不是所有 write,而是後三種。建草稿可以自動,公開發送要停;在本地 branch 改檔可以自動,force push 到受保護分支不行。

Dry-run 要回傳差異,不是回傳「看起來安全」

Agent 先提出 plan,工具端計算實際 effect:

{
  "operation": "waitlist.promote",
  "resource": "class-2026-09-18-0700",
  "expected_revision": 41,
  "dry_run": true,
  "idempotency_key": "task-991-step-4"
}

工具應回傳:

{
  "current_revision": 41,
  "effects": {
    "actor_position": {"from": 6, "to": 5},
    "other_users_displaced": 1,
    "notifications_sent": 1
  },
  "reversibility": "compensating_only",
  "approval_required": true
}

如果 dry-run 只重述模型打算做什麼,它沒有提供新的安全證據。真正有用的是由下游系統計算受影響資源、其他人、通知與可逆性。

四個控制分別處理四種失敗

revision check  → 防止 Agent 對過期狀態動手
idempotency key → 防止 timeout 重試造成重複副作用
transaction     → 保住同一資料庫內的一致性
compensation    → 對已跨出 transaction 的效果做反向處理

它們不能互相代替。Idempotency 能避免重複扣款,不能判斷第一次扣款是否正確;transaction 能一起更新名額和帳單,不能收回已寄出的信;補償操作可以退款,不能讓客戶忘記已看到的錯誤價格。

一個簡化的 dispatcher 可以把風險留在工具端:

def execute(proposal, store, policy):
    prior = store.result_for(proposal.idempotency_key)
    if prior is not None:
        return prior

    current = store.revision(proposal.resource)
    if current != proposal.expected_revision:
        raise ConflictError("resource changed; re-plan required")

    preview = store.preview(proposal)
    if preview.reversibility in {"compensating_only", "irreversible"}:
        policy.require_bound_approval(proposal, preview)

    result = store.commit(proposal)
    store.record_result(proposal.idempotency_key, result)
    return result

這段程式沒有讓 Agent 自己決定「這次應該可以例外」。只要 resource revision 改了,就回去重新規劃;批准也要同時綁定 proposal 與 preview,避免人同意一個效果,實際執行另一個。

補償失敗也要是一級狀態

Saga pattern 常把跨服務流程拆成多個步驟,每一步準備補償。例如訂旅行可以是訂機票、訂飯店、扣款;第二步失敗後取消第一步。但外部供應商可能拒絕取消,補償本身也可能 timeout。

因此狀態不能只有 success/failed:

planned → approved → executing → completed
                         │
                         ├→ compensating → compensated
                         │                └→ compensation_failed
                         └→ uncertain_outcome

uncertain_outcome 很重要。API timeout 時,不知道對方是否已執行,不能直接重送一遍;應先用 idempotency key 或查詢介面確認。compensation_failed 則必須進人工 queue,附上原操作、已完成副作用、聯絡對象與處理期限。

上線前至少測這五件事

[ ] 相同 idempotency key 重送兩次,只產生一次效果
[ ] revision 改變後,舊 proposal 一定被拒絕
[ ] approval 後改參數,原 token 失效
[ ] commit 成功但回應 timeout,不會盲目重做
[ ] compensation 失敗,會進人工處理並保留完整 trace

這些測試看起來不像 AI,卻正是 Agent 最需要的安全工程。模型可能永遠無法百分之百理解人際後果,系統至少可以讓高傷害操作在落地前多一道可計算的差異與確認。

下一篇網安模型能找出這些漏洞,也能拿同樣能力攻擊它們;我們來看「造出更強防守者」為什麼同時也在降低攻擊門檻。

本篇的鎖

  • 鎖是什麼:可逆性分級、effect preview、revision check、idempotency、transaction 與 compensation workflow。
  • 想攔什麼:Agent 對過期狀態動手、重試造成重複效果,以及操作離開自家系統後無法完整倒帶。
  • 破口在哪:補償不是 rollback,外部通知、資料外洩與他人權益可能已無法還原;補償本身也會失敗。
  • 怎麼補:高影響操作先由下游產生 effect diff,再做綁定批准;把 uncertain outcome 與 compensation failed 納入正式狀態和人工 queue。

參考與來源


上一篇
第 15 篇|看見異常後怎麼停下來——警示、撤銷權限與 containment plan
下一篇
第 17 篇|同一個 Zero-day 能力,既能修補也能攻擊
系列文
我也希望安全第一——然後我們看看這些鎖是怎麼一個個被撬開的19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言