ArangoDB認證閘道失效?
1. 簡介
分散式伺服器架構經常由多個獨立元件組成 — HTTP 閘道、認證過濾器、路由層以及腳本執行環境(Scripting runtime) — 每個元件都以自己的方式解析相同的傳入請求(Incoming request)。當兩個元件對單一請求路徑或單一請求欄位的實際意義產生歧見時,這種歧見本身就成為可被利用的介面(Exploitable surface),無論任一元件自身的邏輯是否個別正確。這份報告分析了這樣的一個案例,該案例是從一篇揭露的研究文章中重建而來,描述了 ArangoDB 多模型資料庫伺服器(Multi-model database server) 中兩個串連的漏洞 [1] 。第一個缺陷源於 認證閘道與請求路由器之間原始路徑與解碼後路徑的不匹配 ;第二個缺陷則源於 單一由客戶端控制的布林值欄位,該欄位會靜默地在沙箱化與特權執行環境之間進行切換 。這兩個缺陷共享一個架構上的根本原因: ArangoDB 的授權決策是衍生自攻擊者提供的資料,而非來自請求路徑上每個元件所共享的規範性單一解讀點。
2. 漏洞一:原始路徑與解碼後路徑的授權閘道
認證閘道 透過檢查原始的、未解碼的請求路徑,將請求分類為「公開」或「受保護」,並將任何不以底線開頭的路徑視為無需認證即可提供服務的安全路徑 [1] :
- // arangod/Actions/RestActionHandler.cpp:142 — hasAllowedUnauthenticatedPath()
- // This function is the ENTIRE authorization gate for non-"/_" paths.
- // It reads request()->requestPath(), which is the raw URL exactly as it
- // arrived on the wire — no percent-decoding has happened yet at this point.
- auto const& path = request()->requestPath();
- return auth->authenticationSystemOnly() && // system-auth mode is default-on
- !path.empty() && !path.starts_with("/_"); // "public" iff raw path lacks a leading "/_"
程式 1 — 決定請求是否可以跳過認證的原始路徑授權檢查。
幾十行之後,在負責將請求分派給處理常式的單獨模組中,相同的路徑在進行路由決策之前會先進行百分比解碼 [1] :
- // arangod/Actions/actions.cpp:96 — TRI_LookupActionVocBase()
- // By the time routing happens, the path has already been percent-decoded,
- // so "%5f" (the URL-encoded form of "_") has become a literal underscore.
- auto suffixes = request->decodedSuffixes(); // %5f decodes to "_"
- auto name = join(suffixes, '/'); // yields "_api/simple/..."
程式 2 — 路由器從解碼後的路徑解析處理常式,此時閘道已經根據原始路徑做出了決策。
由於閘道讀取的是網路傳輸形式,而路由器讀取的是解碼後的形式,像
/%5fapi/simple/all
這樣的路徑會被閘道分類為公開 — 因為它並非以文字
/_
開頭 — 但路由器會將其解析為需要認證的特權
/_api/simple/all
動作。在預設設定中,繞過閘道還會將呼叫者升級為超級使用者安全環境,因為
/_
命名空間之外的任何路徑都被假定為屬於無特權、自行託管的應用程式路由
[1]
。以下序列純粹根據上述兩個函式重建了攻擊路徑。
sequenceDiagram
participant Attacker
participant Gate as AuthGate
(raw path check)
participant Router as Router
(decoded path lookup)
participant Handler as Privileged _api Handler
Attacker->>Gate: PUT /%5fapi/simple/all (no credentials)
Note over Gate: reads raw path "/%5fapi/..."
does not start with "/_" -> treated as PUBLIC
Gate->>Router: forward request, unauthenticated, superuser context
Note over Router: decodedSuffixes() turns "%5f" into "_"
resolved name = "_api/simple/all"
Router->>Handler: dispatch to restricted collection handler
Handler-->>Attacker: 200 OK - collection documents, incl. user/root records
圖1 — 原始路徑閘道與解碼後路徑路由器在分類上產生分歧,導致在未認證且具備超級使用者環境的情況下呼叫受限處理常式。
3. 漏洞二:客戶端控制的執行環境選擇
第二個缺陷完全不涉及路徑編碼;它涉及背景任務 API 的 JSON request body 中的單一欄位。該欄位決定產生的伺服器端 JavaScript 工作是在沙箱中執行,還是具有不受限制的、等同 root 的檔案系統和網路存取權限 [1] :
- // arangod/RestHandler/RestTasksHandler.cpp:204 — registerTask()
- // isSystem is read directly out of the attacker-supplied HTTP body with
- // no additional authorization check beyond "has write access to a DB".
- bool isSystem = VelocyPackHelper::getBooleanValue(body, "isSystem", false);
- // ...
- Task::createTask(id, name, exec, &_vocbase, command, isSystem, res);
程式 3 — 選擇特權的布林值直接取自客戶端輸入。
關鍵在於,同樣建立系統任務的等效內部 JavaScript API 確實執行了檢查 — 只是這個防護機制並未複製到到達相同程式碼路徑的 HTTP 處理常式上 [1] :
- // arangod/V8Server/v8-dispatcher.cpp:161 — guard present on the OTHER entry point
- // This check exists on the internal JS API for creating tasks, but the
- // HTTP RestTasksHandler above never calls through this guard.
- if (isSystem && !securityContext.isInternal())
- throw FORBIDDEN("Only internal context may create system tasks");
程式 4 — 遺漏的檢查,存在於一個進入點,但在執行相同基礎操作的另一個進入點上卻不存在。
當設定
isSystem:true
時,排程的工作會在伺服器的 Internal 環境中執行,解鎖主機上的任意檔案讀寫以及沙箱化環境否則會拒絕的 Outbound HTTP request
[1]
。由於資料庫程序在其預設部署映像中以 root 身份運行,因此僅擁有一個資料庫寫入存取權的已認證使用者 — 例如透過破解『漏洞一』所洩露的弱 root 認證雜湊而取得 — 即可藉由將 authorized-key 或排程工作檔案寫入磁碟,進一步升級為完整的主機程式碼執行。。
sequenceDiagram
participant Attacker as Authenticated DB-write user
participant API as HTTP Tasks API
participant Handler as RestTasksHandler::registerTask
participant VM as V8 Task Runtime
Attacker->>API: POST /_api/tasks {"command": "...", "isSystem": true}
API->>Handler: registerTask(body)
Note over Handler: isSystem read straight from JSON body
no internal-context check performed here
Handler->>VM: createTask(..., isSystem=true)
Note over VM: task runs in Internal (god-mode) context, not sandbox
VM->>VM: read /etc/shadow, write ~/.ssh/authorized_keys
VM-->>Attacker: task result persisted to a collection
Attacker->>API: GET /_api/document/{collection}
API-->>Attacker: file contents / confirmation of write
圖2 — 單一由客戶端控制的布林值將執行路徑繞過沙箱,進入伺服器自身的特權執行環境。
4. 根本原因:授權衍生自不可信且解讀分歧的輸入
這兩個缺陷都可歸結為相同的結構模式:在流程中的某一點,系統會根據攻擊者可控的資料(例如路徑字串、JSON 布林值)做出安全相關的決策;然而該決策原本要限制的操作,卻在後續流程中使用了同一份資料的不同讀取方式或不同消費者來執行。漏洞一是 URL 解析研究中稱為授權相關解析器與路由相關解析器之間不一致的典型實例:先前對解析庫的大規模分析發現,百分比編碼、斜線計數和 scheme 處理在各自表現良好的元件之間經常出現分歧,且這種分歧直接對應到認證和請求偽造繞過 [2] 。漏洞二將相同原則推廣到 URL 之外:它表明產生分歧解讀的「元件」不必是解析器 — 它可以僅僅是省略了第一個程式碼路徑(內部 API)中存在的防護機制的第二個程式碼路徑(HTTP 處理常式)。在這兩種情況下,修復方法是架構性的而非表面性的:在任何授權決策之前必須執行單一的規範性解碼/驗證步驟,並且進入特權操作的每個進入點都必須透過一個共享的授權檢查,而不是重新實作它。供應商的補救措施已在版本 3.12.11 中發布,填補了這兩個缺口,且這兩個問題都被分配了評級為嚴重的協同安全公告(Coordinated advisories) [3][4] 。
5. 嚴重性與系統性影響
| 漏洞 | 前置條件 | 影響 | CVSS 3.1 |
|---|---|---|---|
| 漏洞一 — 閘道/路由器不匹配 | 僅需網路存取,預設設定 | 所有資料的未認證讀取/寫入/刪除,認證雜湊暴露 | 9.8 |
| 漏洞二 — 未檢查的特權欄位 | 對一個資料庫的寫入存取權 | 以 root 身份進行任意檔案讀寫;主機完全淪陷 | 9.9 |
這種組合值得注意,因為這兩個錯誤橋接了不同的信任層級:漏洞一將零認證轉換為資料介面超級使用者存取權;漏洞二將任何單一資料庫寫入特權轉換為主機層級的 root 執行。這兩個漏洞彼此並不依賴,但一旦串聯起來,就顯示系統的整體安全狀態不能僅靠逐一審核各元件的授權邏輯來評估。必須在系統的接口處進行評估──也就是一個元件的輸出交給另一個元件作為輸入的地方,因為雙方可能對同一個請求採用略有不同的標準化表示。
6. 結論
這個核心教訓不僅適用於此 ArangoDB 特定伺服器:只要是閘道、路由器或中介層在執行授權檢查時,該檢查的可信度僅取決於它能否保證所檢查的資料在位元層級與語意層級上,與後續處理器實際使用的資料完全一致。在 ArangoDB 的案例中,一次未正規化的解碼步驟,或一次未防護的次要程式路徑,就足以讓原本正確撰寫的存取控制被繞過。因此,安全的系統架構必須將『請求解釋』(Request interpretation)本身視為安全邊界,並進行端到端驗證,而不是假設各元件的局部正確性會自動組合成整體的正確性。