摘要

此技術研究報告探討了 CVE-2026-21643,這是一個在 FortiClient EMS 7.4.4 版本中發現的嚴重預先驗證 SQL injection 漏洞。該漏洞源於應用程式 middleware 層中輸入清理的根本性缺失,特別是在多租戶環境中處理 "Site" HTTP header 時。透過在初始化資料庫連線參數的過程中利用不安全的字串插入,未經身分驗證的遠端攻擊者可以繞過安全控管並執行任意 SQL 指令。本報告剖析了其架構缺陷,提供了受影響元件的詳細程式碼層級分析,並評估了 7.4.5 版本中實施的修復策略。

FortiClient EMS 的「Site」標頭與 f-string 如何聯手奉上 SQL injection 大禮 | 資訊安全新聞

1. 架構分析與請求處理流程

FortiClient EMS 7.4.4 的安全態勢深受其請求處理架構的影響。該應用程式使用 Django 框架建置,利用一系列 middleware 元件來處理日誌記錄、多租戶與身分驗證。此版本的一個關鍵設計特性是這些 middleware 元件的特定排序。在 7.4.4 版本中,開發團隊對 middleware 堆疊進行了重大重構,以滿足更複雜的多租戶需求並改進 API 日誌記錄 [1]

graph TD A[Incoming HTTP Request] --> B[Apache / mod_wsgi] B --> C[Django Middleware Stack] C --> D[ApiLogMiddleware:
Logs the raw request] D --> E[SiteMiddleware:
Extracts Site header] E --> F[PostgresConnection:
Sets search_path] F --> G[AuthMiddleware:
Validates credentials] G --> H[View / Controller:
Executes business logic] style E fill:#ffeaa7,stroke:#d63031,stroke-width:2px style F fill:#ffeaa7,stroke:#d63031,stroke-width:2px

如架構圖所示, SiteMiddleware 及其後的 PostgresConnection 初始化程序被置於 AuthMiddleware 上游 [1] 。這意味著,系統在驗證請求者的身分或授權之前,便會依據使用者提供的 header 來建立該次 session 的資料庫環境。雖然此順序對於將請求路由至正確的租戶資料庫是必要的,但它也將沉重的安全負擔加諸於 SiteMiddleware ,要求其必須嚴格驗證租戶識別碼。在 7.4.4 版本中,這項驗證嚴重缺失。

2. 技術根本原因:不安全的字串插入

此漏洞是一個經典的範例,說明了未能維持資料平面(使用者輸入)與控制平面(SQL 指令)之間明確界線所導致的問題。 SiteMiddleware 提取 "Site" header 後,僅經過極少的處理就將其存入請求的中繼資料中 [1]

2.1 Middleware 資料流與提取過程

以下來自 site_middleware.py 的反編譯程式碼片段說明了提取邏輯。請注意,對使用者控制的 header 所做的唯一轉換是將其轉換為小寫:

  1. # site_middleware.py - Decompiled logic for header extraction
  2. def _get_header_site(self, request):
  3. # Retrieve the 'Site' header from the HTTP request
  4. site = request.META.get('HTTP_SITE')
  5. if site is not None:
  6. # The value is converted to lowercase but otherwise remains unsanitized
  7. # No allow-list validation or regex filtering is performed here
  8. request.META['SITE'] = site.lower()
  9. return site is not None

這個未經清理的原始小寫字串隨後被傳遞給 PostgresConnection 類別。在一個多租戶系統中,PostgreSQL 的 search_path 用於決定資料庫在執行查詢時應優先使用哪個 schema。這是在單一資料庫實例中隔離租戶資料的常見模式。

2.2 有漏洞的資料庫初始化

關鍵的錯誤發生在 models/utils/postgres_conn.py 中。應用程式使用 Python 的 f-string 來建構 SET search_path 的 SQL 指令。當涉及外部輸入時,這種建構 SQL 的方式本身就具有危險性 [2]

  1. # models/utils/postgres_conn.py - Vulnerable implementation in 7.4.4
  2. class PostgresConnection:
  3. def __init__(self, vdom, ...):
  4. # vdom is the unsanitized value derived from the 'Site' header
  5. self.db_name = f"fcm_{vdom}"
  6. # VULNERABILITY: Direct interpolation of vdom into the SQL string
  7. # The single quotes are intended to wrap the schema name, but can be escaped
  8. self.searchpath = f"SET search_path TO '{self._db_prefix}{self.db_name}', public, addons"
  9. def execute(self, query, ...):
  10. # This searchpath command is executed at the start of every database session
  11. # This happens BEFORE AuthMiddleware has a chance to reject the request
  12. self._connection.execute(self.searchpath)

攻擊者可以利用此漏洞,提供一個包含單引號的 "Site" header 來跳出字串常值,接著使用分號終止 SET 指令,然後插入他們自己的惡意 SQL。例如,一個 x'; SELECT pg_sleep(10)-- 的 payload 將會導致執行以下 SQL:

SET search_path TO 'fcm_x'; SELECT pg_sleep(10)--', public, addons

資料庫引擎將此解讀為兩個獨立的指令。第一個指令將 search path 設為一個不存在或任意的 schema,第二個指令則執行 pg_sleep(10) 函數,導致伺服器回應出現明顯的延遲。 -- 字元則確保原始字串的其餘部分被視為註解而忽略 [1]

3. 版本演進的比較分析

CVE-2026-21643 是一個「單一版本」漏洞,意味著它是在特定的重構週期中被引入,並在緊接的下一版本中被修補。這突顯了大規模程式碼變更所帶來的潛在風險。下表詳細說明了 middleware 堆疊的演變以及該漏洞的生命週期:

版本 Middleware 元件數量 漏洞狀態 技術背景
7.4.3 6 個檔案(舊版) 不受影響 使用了較舊、較嚴謹的資料庫路由邏輯,未使用相同的不安全插入模式。
7.4.4 10 個檔案(重構後) 存在漏洞 引入了新的 middleware 用於日誌記錄和速率限制;重構了資料庫連線邏輯,使用了不安全的 f-string。
7.4.5 10 個檔案(已修補) 已修補 維持了新架構,但將不安全的插入方式替換為參數化識別碼。

7.4.4 版本的重構幅度相當大。 postgres_conn.py 檔案的規模增加了約 14%,反映了新的多租戶路由邏輯所帶來的額外複雜性。不幸的是,這種複雜性也引入了導致 CVE-2026-21643 的安全回歸問題 [1]

4. 攻擊途徑與影響

此漏洞之所以特別嚴重,是因為它可以透過預先驗證的端點觸發。其中一個這樣的端點是 /api/v1/init_consts 。該端點用途在為客戶端提供初始化常數,因此不需要有效的 session [1]

此外,7.4.4 版本中的應用程式錯誤處理 middleware 經常會將詳細的資料庫錯誤訊息返回給客戶端。這為攻擊者提供了即時反饋,使他們能夠快速調整 SQL injection 的 payload。由於 BruteForceProtection middleware 也被安排在資料庫初始化之後,攻擊者可以發送大量惡意請求而不受標準登入速率限制機制的約束 [1]

安全提示: 在多租戶環境中,執行任意 SQL 指令的能力可能導致跨租戶資料洩露、未經授權的系統設定修改,甚至完全攻陷底層的資料庫伺服器。

5. 修復措施與最佳實務

FortiClient EMS 7.4.5 中實施的修復遵循了業界防止 SQL injection 的最佳實務。開發人員不再手動建構字串,而是改用 psycopg.sql 模組,該模組提供了一種安全的方式來處理 SQL 識別碼 [2]

5.1 安全的實作方式

修補後的程式碼使用了 Identifier 類別,確保 schema 名稱能由資料庫驅動程式本身進行正確的引用和跳脫,而不是依賴應用程式層級的字串格式化:

  1. # models/utils/postgres_conn.py - Secure implementation in 7.4.5
  2. from psycopg.sql import SQL, Identifier
  3. # The schema name is constructed as before
  4. schema_name = f'{self._db_prefix}{self.db_name}'
  5. # SECURE: Using SQL.format() with Identifier()
  6. # This ensures the schema_name is treated as a literal identifier
  7. self.searchpath = SQL('SET search_path TO {}, public, addons').format(Identifier(schema_name))

這種方法有效地消除了漏洞,確保 schema_name 中的任何特殊字元(如單引號或分號)都會根據 PostgreSQL 的識別碼規則進行跳脫,從而防止它們被解讀為 SQL 指令 [1]

6. 結論

對 CVE-2026-21643 的分析強調了在架構轉換期間實施安全編碼實務的極端重要性。該漏洞並非源於複雜的邏輯錯誤,而是在一個新重構的元件中根本未能應用參數化查詢。藉由將資料庫初始化置於身分驗證之前,並使用不安全的字串插入,FortiClient EMS 7.4.4 製造了一個重大的安全缺口。7.4.5 版本中的快速解決方案證明了使用現代資料庫驅動程式內建安全功能的有效性。使用 FortiClient EMS 的組織必須確保其已升級至 7.4.4 版本之後,以保護其多租戶基礎架構免受未經身分驗證的攻擊。