題目與適用情境
一家 B2B 文件服務使用共享資料庫。使用者可以屬於多個租戶,並在每個租戶中擁有 viewer、 editor 或 admin 角色;文件也可以單獨分享給同租戶成員。服務提供單筆讀取、修改、刪除、批次 匯出及非同步產生下載包的 API。JWT 已完成簽章、到期時間與受眾驗證,但舊程式會先依用戶端提供的 document_id 全域查詢文件,再判斷請求是否已登入。
攻擊者從稽核紀錄、分享連結或另一個 API 取得有效 UUID 後,把自己的文件 ID 換成該 UUID。UUID 很難列舉,但伺服器若沒有檢查「目前主體能否在目前租戶對這個物件執行這項動作」,仍會回傳或修改 文件。OWASP 把這類問題稱為 Broken Object Level Authorization(BOLA);在常見 Web 安全語境中 也稱 Insecure Direct Object Reference(IDOR)。物件識別碼可以位於路徑、查詢參數、請求標頭、 JSON、GraphQL 變數、檔名或批次清單中。
兩份公開 API 面試準備材料都直接要求解釋或測試 IDOR。2026 年一則公開討論仍在追問「加密物件 ID 能否修復 IDOR」,顯示「隱藏 ID」仍是候選人需要拆解的實際誤區。OWASP API Security Top 10 的 API1:2023 與 2026 年公開實證研究進一步提供風險與技術邊界;這些證據支持題目的代表性,不用來推斷 特定公司或面試頻率。
本題歸入後端:核心工作是把驗證後的身分內容、租戶關係、物件屬性與動作落實到 API、資料存取、 交易、快取和背景任務的共同授權邊界。現有題庫的 OAuth 文章處理身分與授權碼流程,SQL injection 文章處理查詢結構,SSRF 文章處理出站目標授權;它們都不能取代本題的物件層級存取決策。
面試官考察什麼
第一項訊號是能否精確區分概念。身分驗證回答「呼叫者是誰」;物件層級授權回答「這個主體現在能否 對這個物件做這個動作」。使用者有權呼叫 PATCH /documents/{id},卻無權修改另一個租戶的文件, 屬於 BOLA。一般成員能呼叫只有管理員可用的批次刪除功能,比較接近 Broken Function Level Authorization;能修改自己文件中本來不允許更新的 owner_id,則屬於物件屬性層級授權問題。
第二項訊號是授權模型完整。只比較 document.owner_id == user.id 會漏掉租戶角色、團隊分享、唯讀 權限、權限撤銷和支援人員臨時存取。好的回答會把一次決策寫成:
allow(subject, tenant, action, resource, context) -> decision + reason其中租戶來自經過驗證的成員關係,動作區分 read、update、delete、share、export,資源包含 所屬租戶、狀態及分享關係,情境可包含臨時支援授權與策略版本。沒有符合允許規則時預設拒絕。
第三項訊號是把安全邊界放到查詢和寫入處。先執行 findById(id),再靠某個 controller 補一次檢查, 很容易被批次 API、背景任務、快取和新路由繞過。建議路徑從已授權資料集合查詢,寫入把範圍與版本 條件放進同一條敘述或交易,並檢查受影響資料列數。
第四項訊號是理解縱深防禦的邊界。隨機 UUID、404、限流與 PostgreSQL Row-Level Security(RLS) 都有價值,但沒有一項能取代完整業務授權。PostgreSQL 18 文件也明確說明超級使用者、BYPASSRLS 角色及通常情況下的資料表擁有者會繞過 RLS;策略啟用、連線角色和請求租戶情境都必須驗證。
最後看能否證明「所有路徑都拒絕越權」。只測自己的 GET 成功屬於功能測試。安全測試要使用多個 帳戶、多個租戶、多個角色及已知有效的他人物件 ID,涵蓋讀取、修改、刪除、批次、匯出、下載、 GraphQL、快取命中與非同步執行,還要斷言沒有資料庫變更、物件儲存產物或訊息副作用。
回答前要確認的問題
- 租戶如何選擇? 使用者可以切換作用中的租戶,但用戶端的
X-Tenant-ID只能表達選擇;伺服器
必須用已驗證身分重新確認成員關係,不能把請求標頭直接當成可信租戶。
- 權限由什麼決定? 只有租戶角色,還是也包含文件擁有權、團隊、明確分享、文件狀態與臨時支援
授權?規則越動態,越需要集中策略和一致的稽核原因。
- 哪些動作需要獨立權限? 讀取不會自動包含下載、匯出、分享或刪除。批次操作必須為每個物件做
相同的動作層級判斷。
- 未授權時回傳 403 或 404? 對外物件 ID 若不應暴露存在性,可以把不存在與不可見統一成 404;
同租戶內已知物件但動作不足,可依產品契約回傳 403。無論狀態碼如何,都不能回傳敏感差異。
- 非同步任務採用目前權限或提交時快照? 本題在入列時檢查一次,執行時重新檢查目前權限;撤權後
尚未執行的匯出必須停止。若業務需要不可撤銷的授權快照,應設計成有範圍、有期限的明確授權。
- 支援人員能否跨租戶存取? 允許時走獨立的臨時授權路徑,要求工單、理由、核准、到期和完整
稽核,不能為一般應用連線提供長期全域管理員開關。
- 是否使用 PostgreSQL RLS? 本題以應用層範圍查詢作為主要控制,並用 RLS 防禦遺漏。也可以
選擇其他組合,但要說明資料庫角色、連線池情境、migration 與背景任務如何保持策略成立。
30 秒回答框架
「我會先把授權寫成 subject + tenant + action + resource + context 的決策,並預設拒絕。身分驗證 middleware 只建立主體;作用中租戶必須從已驗證成員關係取得。資料存取層不提供無範圍的文件查詢, 讀取使用 tenantid + documentid,再結合角色、擁有權和分享關係判斷動作;更新與刪除把這些條件 放進同一條敘述或交易,零列受影響就依不可見處理。UUID 只降低列舉,不能取代授權。
批次、快取、下載和非同步匯出都沿用同一規則:快取鍵帶租戶,能力連結限制物件、動作和期限,任務 入列與執行各檢查一次。PostgreSQL RLS 可以兜底,但應用角色不能繞過策略,並要測試連線池切換租戶。 最後用兩個租戶的多角色矩陣替換路徑、內容和批次 ID,斷言回應、資料、檔案與訊息都沒有跨租戶 副作用。」
分步深入解析
第一步:列出授權元組和物件入口
先把業務規則寫成矩陣,避免把角色名稱散落在 controller 中:
| 主體關係 | read | update | delete | share | export | |---|---:|---:|---:|---:|---:| | 租戶 viewer,未分享 | 拒絕 | 拒絕 | 拒絕 | 拒絕 | 拒絕 | | 租戶 viewer,明確分享 | 允許 | 拒絕 | 拒絕 | 拒絕 | 允許或依產品規則 | | 租戶 editor | 允許 | 允許 | 拒絕 | 依策略 | 允許 | | 文件擁有者 | 允許 | 允許 | 依策略 | 允許 | 允許 | | 租戶 admin | 允許 | 允許 | 允許 | 允許 | 允許 |
表格只是本題假設。真實系統要由產品與安全負責人確認,並為每條允許規則寫出理由。預設拒絕表示 新增動作、未知角色、缺少租戶情境或策略錯誤都會安全地失敗。
接著建立物件入口清單:路徑參數、查詢條件、請求內容、批次陣列、GraphQL 節點、分享權杖、物件 儲存鍵、快取鍵、訊息和背景任務。OWASP 的定義不要求 ID 是遞增數字;UUID、檔名及一般字串同樣是 物件引用。逐一標註主體來源、租戶來源、動作、資源查找方式和最終副作用,才能發現旁路。
第二步:從驗證後的身分內容建立可信租戶
JWT 驗證通過後取得穩定 user_id,不代表其中長期快取的所有角色仍可信,也不能信任用戶端另傳的 租戶 ID。使用者選擇租戶 tenant_b 時,伺服器查詢或驗證目前有效成員關係,建立只在本請求內生效的 AuthContext:
AuthContext {
user_id,
tenant_id,
membership_id,
roles,
policy_version,
support_grant_id?
}middleware 負責保證情境存在,領域策略仍負責物件與動作判斷。連線池、任務 consumer 和並行請求 不能共用可變的全域租戶變數;每次借出連線都在交易範圍設定情境,結束後清除,避免上一個請求的租戶 洩漏給下一個。
第三步:把租戶範圍放進查詢和資料限制
危險查詢先全域取得物件:
SELECT * FROM documents WHERE id = :document_id;基礎防線至少同時限定可信租戶:
SELECT *
FROM documents
WHERE tenant_id = :auth_tenant_id
AND id = :document_id;若明確分享決定可見性,可以在範圍查詢中加入分享關係,或先取得同租戶資源後交給集中策略引擎判斷。 不能從請求內容讀取 tenant_id 填入條件。對外可以統一把零列解釋成 404,回應上不區分「其他租戶 存在」與「不存在」。
資料表結構要讓錯誤更難發生。分享表、版本表、附件表和匯出項目都保存 tenant_id;唯一鍵與外鍵在 需要時採用 (tenantid, resourceid) 組合,使一個租戶的子資料列無法引用另一個租戶的父資料列。 應用仍要執行授權,資料庫限制負責阻止跨租戶關聯被意外寫入。
第四步:讓授權與寫入位於同一正確性邊界
「讀取文件、在應用判斷、稍後更新」存在檢查後使用競態。權限或文件狀態可能在兩步之間改變。簡單 規則可以直接使用條件更新:
UPDATE documents
SET title = :title, version = version + 1
WHERE tenant_id = :auth_tenant_id
AND id = :document_id
AND version = :expected_version
AND (
owner_id = :user_id
OR :can_edit_tenant_documents
);受影響列數為零時,不再發送訊息、產生稽核成功紀錄或更新快取。複雜分享關係可以在同一交易鎖定 相關成員與資源版本,或讓策略轉成資料庫述詞。關鍵是授權依據與副作用共享明確的交易/版本邊界, 不能檢查舊快照後無條件寫入。
批次操作必須先定義原子性。本題選擇全有或全無:在同一可信租戶和動作下解析全部 ID,去重後查詢 已授權集合;數量與輸入不一致就整批拒絕,不能悄悄匯出可見子集讓呼叫端探測存在性。若產品選擇逐項 結果,也要逐物件授權,並對未授權項使用不會洩漏的統一結果。
第五步:涵蓋快取、下載與非同步任務
快取鍵至少包含租戶和資源版本,例如 document:{tenantid}:{documentid}:{version}。命中快取只 取得資料,目前請求仍要通過動作授權。若快取策略決策,鍵必須涵蓋主體、租戶、動作、資源、關係或 策略版本,並在撤權時失效;複雜系統通常更安全的做法是快取關係資料,再重新計算短小決策。
預先簽章的下載 URL 是一種短期能力。一旦簽發,持有者可能在有效期間直接存取儲存服務,因此簽發前 檢查 download 權限,並把物件、動作、到期時間及內容處置綁定到連結。高敏感文件使用短期限、單次 或代理下載與撤銷機制。簽章證明連結由伺服器簽發,不會讓原本無權的呼叫者自動取得授權。
非同步匯出在兩個時點檢查:API 入列前驗證每份文件的 export 權限;工作程序執行時用任務中的主體 與租戶引用重新載入目前成員關係和資源權限。權限已撤銷、文件換租戶或支援授權到期就停止,並確保 尚未產生可下載內容。任務載荷不能接受呼叫端自稱的管理員標記,也不能保存永久有效的角色陣列。
第六步:把 PostgreSQL RLS 當作可驗證的縱深防禦
共享資料表可以啟用 RLS,讓策略依交易內設定的可信租戶篩選既有資料列,並用 WITH CHECK 約束新增 或修改後的資料列。沒有適用策略時預設拒絕,是理想的失敗方式。但上線前必須驗證:
- 應用連線角色不是超級使用者,沒有
BYPASSRLS,也不是會預設繞過策略的資料表擁有者;需要時使用
FORCE ROW LEVEL SECURITY;
- 每個交易都設定目前租戶,連線歸還 pool 前清除;缺少情境時拒絕,不能使用預設租戶;
USING涵蓋可見舊列,WITH CHECK涵蓋 insert 和 update 後的新列;- migration、備份、背景任務和支援工具使用獨立角色及明確流程;
- 多個 permissive 策略預設以
OR組合,新增策略不會意外擴大存取;複雜子查詢策略還要評估並行
快照與效能。
RLS 無法表達所有產品關係,也無法保護繞過資料庫的物件儲存或搜尋索引。應用策略負責完整業務語意, RLS 負責在某條查詢漏加租戶條件時阻止外洩;兩層要用同一權限矩陣做一致性測試。
第七步:設計錯誤、稽核和可證偽測試
對跨租戶或不可見物件,本題回傳統一 404 和固定回應結構。拒絕路徑不回傳文件標題、租戶名稱、 擁有者、版本、檔案大小或不同耗時提示。限流可以降低列舉和稽核雜訊,但安全結論不能依賴攻擊者猜 不到 UUID。
稽核記錄 actor、可信 tenant、action、資源識別碼的受控表示、decision、reason code、policy version、 support grant 和 trace ID。日誌不保存文件正文、下載能力或完整 JWT。異常指標包含跨租戶拒絕、同一 主體短時間存取大量不存在物件、策略錯誤、RLS 拒絕、撤權後任務終止及支援授權使用。
測試矩陣至少包含:
- 兩個租戶,每個租戶有 owner、viewer、editor、admin,再加入已撤權使用者和臨時支援人員;
- 自己的物件、同租戶未分享物件、明確分享物件、其他租戶有效 UUID 和不存在 UUID;
- list、GET、PATCH、DELETE、share、bulk export、GraphQL、download、cache hit 和 worker execute;
- 路徑、查詢、JSON、批次陣列、巢狀 GraphQL 變數、檔案鍵與任務載荷中的 ID 替換;
- 讀取拒絕時回應不洩漏,寫入拒絕時列版本、分享關係、檔案、訊息、搜尋索引與稽核成功事件都不變;
- 撤權與更新並行、連線池連續切換租戶、RLS 情境缺失、應用角色設定錯誤和快取舊權限。
在 CI 中從授權矩陣產生正向及負向案例,並要求新端點必須登記資源與動作。掃描器能發現一部分可列舉 路徑,但不了解業務擁有權;多帳戶、已知有效的他人物件及副作用斷言才是本題的核心證據。
高品質示範回答
「我會把這次缺陷定義成 BOLA:JWT 已經證明呼叫者身分,API 仍缺少對目標文件的動作層級授權。 UUID 降低列舉機率,不會改變授權結論。我先建立 subject、tenant、action、resource、context 的 矩陣,預設拒絕。使用者可以選擇作用中租戶,但伺服器必須從已驗證成員關係產生租戶情境。
資料存取不再向業務路由暴露全域 findById。讀取先依可信 tenantid + documentid 縮小集合,再 依角色、擁有權與分享關係判斷 read、update、delete、share 或 export。簡單更新直接把租戶、 物件、動作條件與版本放入同一條條件敘述,零列受影響就停止所有副作用;複雜策略在同一交易固定相關 版本。子資料表使用複合租戶外鍵阻止跨租戶關聯。
我會把旁路納入同一模型。批次匯出依每個 ID 授權,本題選擇全有或全無;快取鍵帶租戶且命中後仍 重新授權;下載連結在簽發時檢查 download,綁定物件和短期限;非同步任務入列與執行都檢查,確保 撤權在執行前生效。PostgreSQL RLS 用於兜底,但應用角色不能擁有 BYPASSRLS 或資料表所有權,並 測試連線池交易情境不會串租戶。
最後我用兩個租戶的多角色帳戶,把一個已知有效的他人 UUID 放入路徑、JSON、批次與 GraphQL,涵蓋 讀寫刪、匯出、下載、快取及任務。每次拒絕都檢查回應不洩漏,並斷言資料庫版本、檔案、訊息和搜尋 索引沒有變化。這樣能逐條證明物件層級授權,並同時涵蓋登入、錯誤回應與副作用。」
常見錯誤
- 把 UUID、Base64 或加密 ID 當成修復 → ID 可能從日誌、分享或其他 API 外洩,持有有效引用仍可
越權 → 每次依主體、租戶、物件及動作執行伺服器端授權,隨機 ID 只做縱深防禦。
- 驗證 JWT 後允許讀取任意 ID → 身分驗證只確認主體,未證明其能存取該物件 → **從可信成員關係
建立租戶情境,再做物件層級決策。**
- controller 先全域查詢,接著手寫 owner 檢查 → 新路由、批次、快取和背景任務容易遺漏,團隊
分享也會被錯誤拒絕 → 提供租戶範圍資料存取與集中策略,預設拒絕。
- 只測 GET → PATCH、DELETE、匯出、GraphQL 和下載可能仍洩漏或改變狀態 → **依資源入口和動作
產生多帳戶負向矩陣。**
- 批次 API 過濾無權 ID 後繼續成功 → 回傳數量與內容可能成為物件存在性探針,呼叫端也難以理解
部分成功 → 預先定義全有或全無/逐項契約,並對每個物件授權。
- 把 RLS 視為自動安全 → 資料表擁有者、超級使用者、
BYPASSRLS、缺少情境或 permissive 策略
組合都可能破壞隔離 → 驗證角色、交易情境、USING/WITH CHECK 及故障模式。
- 只斷言回傳 403 或 404 → 拒絕前可能已經寫入資料庫、發送訊息或產生檔案 → **同時斷言所有
持久化及外部副作用保持不變。**
- 為了排查而記錄完整物件和權杖 → 安全日誌本身形成新的資料外洩 → **記錄最小身分、動作、原因
和受控資源識別碼。**
追問及應對
追問一:既然 UUIDv4 幾乎猜不到,還需要物件層級授權嗎?
需要。物件引用可能經分享連結、瀏覽器歷史、日誌、通知、分析系統、另一個 API 或誤發訊息外洩。 UUID 只能降低盲目列舉成功率,不能表達持有者、租戶、動作或期限。測試時直接給攻擊帳戶一個已知 有效的他人 UUID;若仍能存取,就已經證明授權缺失。
追問二:跨租戶物件統一回傳 404,會不會妨礙除錯?
外部回應可以統一,內部稽核仍記錄穩定原因碼,例如 RESOURCENOTVISIBLE、ACTION_DENIED 或 TENANTCONTEXTINVALID。維運人員透過受控日誌和 trace ID 排查,呼叫端看不到物件是否存在。若 同租戶協作需要明確的「無編輯權限」,可以在已證明物件對該使用者可見後回傳 403;這要成為一致的 API 契約。
追問三:使用者被移出租戶時,已經排隊的匯出任務怎麼辦?
任務提交成功不表示獲得永久讀取權。本題在執行前重新載入成員關係及每個資源的 export 權限;撤權 後任務進入拒絕狀態,刪除暫存檔,也不簽發下載連結。若合規流程要求提交時凍結權限,應建立明確的 短期授權快照,記錄範圍、核准者和到期時間,不能暗中沿用過期 JWT 角色。
追問四:應用已在每條查詢加入 tenant_id,RLS 還有什麼價值?
它能擋下漏加條件的新查詢及部分直接資料庫路徑,縮小單點疏漏的影響。不過它增加連線情境、角色、 migration 與策略維護成本,也無法保護搜尋索引、快取和物件儲存。高敏感共享表適合雙層防禦;採用前 要證明應用角色不繞過 RLS、缺少租戶時預設拒絕、連線池不串情境,並用同一矩陣測試兩層結果一致。
追問五:支援工程師需要臨時查看客戶文件,如何避免長期後門?
建立獨立的 just-in-time 支援授權:綁定工單、目標租戶、允許動作、核准者、短期限和理由;敏感動作 可要求雙人核准。支援 API 與一般 API 分開,顯示明顯的工作階段狀態,禁止批次下載,並記錄每次物件 存取。授權到期立刻失效,定期審查使用情況。一般應用角色和通用服務權杖不取得跨租戶權限。