三個新的「_pointer()」介面已新增至 SQLite 3.20.0 (2017-08-01)
在郵件清單上,對於這些新介面的目的、它們被引進的原因,以及它們解決的問題,迅速出現了疑問和混淆。本文嘗試回答這些問題並消除混淆。
對於 SQLite 擴充功能來說,在子元件之間或擴充功能與應用程式之間傳遞非 SQL 值有時很方便。一些範例
在FTS3 擴充功能中,MATCH 運算子(執行全文搜尋)需要將匹配項目的詳細資料傳遞給snippet()、offsets() 和matchinfo() 函式,以便這些函式可以將匹配的詳細資料轉換為有用的輸出。
為了讓應用程式新增新的擴充功能至 FTS5,例如新的分詞器,應用程式需要指向「fts5_api」物件的指標。
在 CARRAY 擴充套件 中,應用程式需要告知擴充套件包含擴充套件實作的表格值函式的資料的 C 語言陣列位置。
傳遞此資訊的傳統方式是將 C 語言指標轉換為 BLOB 或 64 位元整數,然後使用 sqlite3_bind_blob()、sqlite3_result_blob()、sqlite3_value_blob() 等一般介面或整數等價物,透過 SQLite 傳送該 BLOB 或整數。
傳遞指標就像傳遞整數或 BLOB 一樣容易、有效,而且在應用程式元件彼此友善的環境中運作良好。然而,傳遞指標作為整數和 BLOB 允許惡意的 SQL 文字偽造無效指標,可能會造成惡作劇。
例如,snippet() 函式的第一個引數應該是 FTS3 表格的特殊欄位,其中包含指向 fts3cursor 物件的指標,該物件包含關於目前全文搜尋比對的資訊。該指標以前傳遞為 BLOB。例如,如果 FTS3 表格命名為「t1」,並有一個名為「cx」的欄位,可以寫成
SELECT snippet(t1) FROM t1 WHERE cx MATCH $pattern;
但是,如果駭客能夠執行任意 SQL,他可能會執行稍微不同的查詢,如下所示
SELECT hex(t1) FROM t1 WHERE cx MATCH $pattern;
由於指標在 t1 表格的 t1 欄位中傳遞為 BLOB(在舊版本的 SQLite 中),因此此類查詢會以十六進位制顯示指標的值。攻擊者可以修改該指標,嘗試讓 snippet() 函式修改應用程式位址空間其他部分的記憶體,而不是它應該操作的 fts3cursor 物件
SELECT snippet(x'6092310100000000') FROM t1 WHERE cx MATCH $pattern;
從歷史上來看,這不被視為威脅。論點是,如果惡意代理能夠將任意 SQL 文字注入應用程式,則該代理已經完全控制應用程式,因此讓惡意代理偽造指標並不會賦予代理任何新功能。
在大部分情況下,潛在攻擊者無法注入任意 SQL,因此大部分使用 SQLite 的情況都不會受到上述攻擊的影響。但有一些值得注意的例外情況,例如:
Webkit 的 WebSQL 介面允許任何網頁在 Chrome 和 Safari 的瀏覽器中執行任意 SQL。理論上,該任意 SQL 應在沙盒中執行,即使遭到攻擊也不會造成危害,但實際上該沙盒的安全性不如人們想像中高。2017 年春天,有一組駭客能夠使用一系列攻擊手法入侵一台 iMac,其中一個手法是損壞傳遞給 SQLite 資料庫的 snippet() FTS3 函數的 BLOB 值指標,而該資料庫是透過 Safari 中的 WebSQL 介面執行的。
據我們所知,在 Android 上有許多服務會盲目執行從網路上不安全角落下載的不可靠應用程式傳遞給它們的任意 SQL。理論上,Android 服務對於執行來自未經審查來源的 SQL 應更加謹慎。本文作者沒有任何具體的反例,但他聽說過有這種情況。即使所有 Android 服務都更加小心,並適當地審查他們執行的所有 SQL,也很難審查所有服務以驗證它們是否安全。因此,注重安全的人員熱衷於確保無法透過傳遞任意 SQL 文字來進行攻擊。
版本控制系統 Fossil(設計和編寫的目的是支援 SQLite 開發)允許輕度受信任的使用者輸入任意 SQL 以產生問題追蹤報告。該 SQL 使用 sqlite3_set_authorizer() 介面進行清除,且從未發現任何漏洞。但這是一個潛在敵意代理人能夠將任意 SQL 注入系統的範例。
第一次嘗試關閉指標傳遞中的安全漏洞是防止指標值被偽造。這是透過讓發送者使用 sqlite3_result_subtype() 為每個指標附加子類型,並讓接收者使用 sqlite3_value_subtype() 驗證該子類型,並拒絕具有不正確子類型的指標來完成的。由於無法使用純粹的 SQL 為結果附加子類型,這可以防止指標使用 SQL 進行偽造。傳送指標的唯一方法是使用 C 程式碼。如果攻擊者可以設定子類型,則他也可以在沒有 SQLite 協助的情況下偽造指標。
使用子類型來識別有效的指標防止了 WebSQL 漏洞。但事實證明這是一個不完整的解決方案。
在指標上使用子類型可以防止使用純粹的 SQL 偽造指標。但子類型無法阻止攻擊者讀取指標的值。換句話說,指標值上的子類型可以防止使用類似這樣的 SQL 陳述式進行攻擊
SELECT snippet(x'6092310100000000') FROM t1 WHERE cx MATCH $pattern;
snippet() 的 BLOB 引數沒有正確的子類型,因此 snippet 函式會忽略它,不會對任何資料結構進行變更,並無害地傳回 NULL。
但使用子類型無法防止使用類似這樣的 SQL 程式碼讀取指標的值
SELECT hex(t1) FROM t1 WHERE cx MATCH $pattern;
你可能會問,這會造成什麼危害?SQLite 開發人員(包括本文作者)也有相同的疑問。但隨後安全研究人員指出,指標的知識有助於攻擊者規避地址空間隨機化防禦。這稱為「指標洩漏」。指標洩漏本身並非漏洞,但它有助於攻擊者有效利用其他漏洞。
允許擴充元件安全地彼此傳遞私人資訊,且不引入指標洩漏,需要新的介面
對於 SQL,sqlite3_bind_pointer() 和 sqlite3_result_pointer() 建立的值與 NULL 無法區分。嘗試使用 hex() 函式來讀取指標值的 SQL 陳述式會取得 SQL NULL 答案。找出值是否有關聯指標的唯一方法是使用 sqlite3_value_pointer() 介面搭配適當的類型字串 T。
sqlite3_value_pointer() 讀取的指標值無法由純粹的 SQL 產生。因此,SQL 無法偽造指標。
sqlite3_bind_pointer() 和 sqlite3_result_pointer() 產生的指標值無法由純粹的 SQL 讀取。因此,SQL 無法洩漏指標的值。
這樣一來,新的指標傳遞介面似乎解決了在 SQLite 中從一個擴充元件傳遞指標值到另一個擴充元件時所伴隨的所有安全性問題。
在 sqlite3_bind_pointer()、sqlite3_result_pointer() 和 sqlite3_value_pointer() 的最後一個參數中的「指標類型」用於防止將用於一個擴充元件的指標重新導向到另一個擴充元件。例如,在不使用指標類型的情況下,攻擊者仍然可以使用類似這樣的 SQL 存取包含 FTS3 和 CARRAY 擴充元件 的系統中的指標資訊
SELECT ca.value FROM t1, carray(t1,10) AS ca WHERE cx MATCH $pattern
在上述陳述中,MATCH 運算子產生的 FTS3 游標指標會傳送到 carray() 表值函數中,而不是其預期的接收者 snippet()。carray() 函數將指標視為指向整數陣列的指標,並逐一傳回每個整數,因此會洩漏 FTS3 游標物件的內容。由於 FTS3 游標物件包含指向其他物件的指標,因此上述陳述會造成指標外洩。
但是,由於指標類型,上述陳述無法運作。MATCH 運算子產生的指標類型為「fts3cursor」,但 carray() 函數預期接收類型為「carray」的指標。由於 sqlite3_result_pointer() 上的指標類型與 sqlite3_value_pointer() 呼叫上的指標類型不符,因此 sqlite3_value_pointer() 會在 carray() 中傳回 NULL,並因此向 CARRAY 擴充元件發出已傳遞無效指標的訊號。
指標類型為靜態字串,理想上應為直接嵌入在 SQLite API 呼叫中的字串常數,而非從其他函數傳入的參數。曾考慮使用整數值作為指標類型,但靜態字串提供了更大的名稱空間,可減少不相關擴充元件之間發生意外類型名稱衝突的機率。
「靜態字串」是指一個零終止的位元組陣列,在程式執行期間固定且不變。換句話說,指標類型字串應為字串常數。相對地,「動態字串」是一個零終止的位元組陣列,儲存在從堆積中配置的記憶體中,且必須釋放以避免記憶體外洩。請勿將動態字串用作指標類型字串。
多位評論者表示希望將動態字串用於指標類型,並讓 SQLite 取得類型字串的所有權,並在使用完畢後自動釋放類型字串。此設計因以下原因而遭拒絕
指標類型並非設計為靈活且動態。指標類型應為設計時期常數。應用程式不應在執行時期合成指標類型字串。提供對動態指標類型字串的支援會導致開發人員誤用指標傳遞介面,藉由建立執行時期合成的指標類型字串。要求指標類型字串為靜態可鼓勵開發人員在設計時期選擇固定的指標類型名稱並將這些名稱編碼為常數字串,進而執行正確的動作。
在 SQLite 中,SQL 層級的所有字串值都是動態字串。要求類型字串為靜態會導致難以建立應用程式定義的 SQL 函數,該函數可以合成任意類型的指標。我們不希望使用者建立此類 SQL 函數,因為此類函數會危害系統安全性。因此,使用靜態字串的要求有助於防禦設計不佳的 SQL 函數損害指標傳遞介面的完整性。靜態字串要求並非完美的防禦,因為老練的程式設計師可以編寫程式碼來解決此問題,而新手程式設計師則可以單純接受記憶體外洩。但藉由聲明指標類型字串必須為靜態,我們希望鼓勵開發人員更仔細思考問題,並避免引發安全性問題,否則他們可能會將動態字串用於指標類型。
讓 SQLite 擁有型別字串會對所有應用程式造成效能成本,即使是不使用指標傳遞介面的應用程式。SQLite 會將值傳遞為 sqlite3_value 物件的執行個體。該物件有一個解構函式,因為 sqlite3_value 物件幾乎用於所有事物,因此會頻繁呼叫。如果解構函式需要檢查是否有需要釋放的指標型別字串,則每次呼叫解構函式時都需要多燒掉一些 CPU 週期。這些週期會累積起來。如果指標傳遞是一種常用的程式設計範例,我們願意承擔額外的 CPU 週期成本,但指標傳遞很罕見,因此對數十億個不使用指標傳遞的應用程式施加執行時間成本似乎不明智,只為了方便少數使用指標傳遞的應用程式。
如果您覺得應用程式需要動態指標型別字串,這強烈表示您誤用了指標傳遞介面。您預期的用途可能不安全。請重新思考您的設計。首先確定您是否真的需要透過 SQL 傳遞指標。或者,找出不同於本文所述指標傳遞介面的機制。
sqlite3_bind_pointer() 和 sqlite3_result_pointer() 常式的最後一個參數是指向程序的指標,用於在 SQLite 完成後處置 P 指標。此指標可以是 NULL,這種情況下不會呼叫解構函式。
當 D 參數不是 NULL 時,表示指標的所有權已轉移給 SQLite。SQLite 會負責在完成使用指標後釋放與指標相關的資源。如果 D 參數是 NULL,表示指標的所有權仍屬於呼叫者,呼叫者負責處置指標。
請注意,解構函式 D 是針對指標值 P,而非字串類型 T。字串類型 T 應為具有無限生命週期的靜態字串。
如果指標的所有權透過提供非 NULL D 參數給 sqlite3_bind_pointer() 或 sqlite3_result_pointer() 傳遞給 SQLite,則所有權將保留在 SQLite 中,直到物件被銷毀。沒有辦法將所有權從 SQLite 轉移回應用程式。
使用 sqlite3_bind_pointer()、sqlite3_result_pointer() 和 sqlite3_value_pointer() 介面附帶在 SQL NULL 值上的指標是暫時且短暫的。指標絕不會寫入資料庫中。指標無法在排序中存活。後者是沒有 sqlite3_column_pointer() 介面的原因,因為無法預測查詢規劃器是否會在從查詢傳回值之前插入排序作業,因此無法得知由 sqlite3_bind_pointer() 或 sqlite3_result_pointer() 插入查詢的指標值是否會存活到結果集中。
指標值必須直接從其產生者流向其使用者,沒有中間運算子或函式。指標值的任何轉換都會銷毀指標,並將值轉換為一般的 SQL NULL。
本文的重點
網際網路是一個越來越惡意的環境。如今,開發人員應假設攻擊者會找到一種方法在應用程式中執行任意 SQL。應用程式應設計為防止任意 SQL 的執行升級為更嚴重的漏洞。
一些 SQLite 擴充套件受益於傳遞指標
指標絕不應透過將它們編碼為其他 SQL 資料類型(例如整數或 BLOB)來交換。請改用旨在促進安全指標傳遞的介面:sqlite3_bind_pointer()、sqlite3_result_pointer() 和 sqlite3_value_pointer()。
使用指標傳遞是一種進階技術,應不常使用且謹慎使用。指標傳遞不應隨意或粗心使用。指標傳遞是一個鋒利的工具,如果使用不當,可能會留下深刻的傷疤。
「指標類型」字串是每個指標傳遞介面的最後一個參數,它應為一個不同的、應用程式特定的字串文字,直接出現在 API 呼叫中。指標類型不應是從較高層級函數傳遞的參數。