小巧、快速、可靠。
選擇其中三項。
應用程式定義的 SQL 函式

1. 摘要

使用 SQLite 的應用程式可以定義自訂 SQL 函式,這些函式會回呼應用程式程式碼來計算結果。自訂 SQL 函式的實作可以嵌入到應用程式程式碼本身,也可以是可載入擴充功能

應用程式定義或自訂的 SQL 函式是使用 sqlite3_create_function() 系列介面建立的。自訂 SQL 函式可以是純量函式、聚合函式或視窗函式。自訂 SQL 函式可以有從 0 到 SQLITE_MAX_FUNCTION_ARG 的任意數量參數。sqlite3_create_function() 介面指定了用於執行新 SQL 函式處理的回呼。

SQLite 也支援自訂表值函式,但它們是透過本文檔未涵蓋的不同機制實作的。

2. 定義新的 SQL 函式

sqlite3_create_function() 系列介面用於建立新的自訂 SQL 函式。此系列的每個成員都是圍繞一個共同核心的包裝器。所有系列成員都完成相同的事情;它們只是具有不同的呼叫簽章。

2.1. 通用參數

傳遞給 sqlite3_create_function() 系列介面的許多參數在整個系列中都是通用的。

  1. db → 第一個參數始終是指向自訂 SQL 函式將在其上運作的資料庫連線的指標。自訂 SQL 函式是為每個資料庫連線單獨建立的。沒有用於建立跨所有資料庫連線工作的 SQL 函式的簡便機制。

  2. zFunctionName → 第二個參數是要建立的 SQL 函式的名稱。名稱通常採用 UTF8 格式,但對於 sqlite3_create_function16(),名稱應採用原生位元組順序的 UTF16 格式。

    SQL 函數名稱的最大長度為 255 個 UTF8 位元組。任何嘗試建立名稱更長的函數都會導致 SQLITE_MISUSE 錯誤。

    可以多次使用相同的名稱呼叫 SQL 函數建立介面。例如,如果兩個呼叫具有相同的函數名稱,但參數數量不同,則會註冊兩個 SQL 函數的變體,每個變體接受不同數量的參數。
  3. nArg → 第三個參數始終是函數接受的參數數量。該值必須是介於 -1 和 SQLITE_MAX_FUNCTION_ARG(預設值:127)之間的整數。值 -1 表示 SQL 函數是一個可變參數函數,可以接受 0 到 SQLITE_MAX_FUNCTION_ARG 之間的任意數量參數。

  4. eTextRep → 第四個參數是一個 32 位元整數旗標,其位元傳達關於新函數的各種屬性。此參數的最初目的是使用以下常數之一指定函數的首選文字編碼

    所有自訂 SQL 函數都將接受任何編碼的文字。編碼轉換將自動進行。首選編碼僅指定針對其最佳化函數實作的編碼。可以指定多個具有相同名稱和相同參數數量但不同首選編碼和用於實作函數的不同回呼的函數,SQLite 將選擇輸入編碼與首選編碼最匹配的回呼集。

    第四個參數最近已擴展其他旗標位元,以傳達關於函數的其他資訊。額外的位元包括

    未來版本的 SQLite 中可能會新增其他位元。

  5. pApp → 第五個參數是一個任意指標,它會傳遞到回呼常式中。SQLite 本身不會使用此指標執行任何操作,除了將其提供給回呼,並在取消註冊函數時將其傳遞給解構器。

2.2. 對相同函數多次呼叫 sqlite3_create_function()

應用程式通常會針對同一個 SQL 函數多次呼叫 sqlite3_create_function()。例如,如果一個 SQL 函數可以接受 2 個或 3 個參數,則會針對 2 參數版本呼叫一次 sqlite3_create_function(),並針對 3 參數版本再次呼叫。兩種變體的底層實作(回呼)可以不同。

應用程式也可以註冊多個具有相同名稱、相同參數數量但不同首選文字編碼的 SQL 函數。在這種情況下,SQLite 將使用首選文字編碼與資料庫文字編碼最匹配的版本的回呼來呼叫函數。透過這種方式,可以提供針對 UTF8 或 UTF16 最佳化的相同函數的多個實作。

如果多次呼叫 sqlite3_create_function() 指定相同的函數名稱、相同的參數數量和相同的首選文字編碼,則第二次呼叫的回呼和其他參數將覆蓋第一次呼叫,並且將呼叫第一次呼叫的解構器回呼(如果存在)。

2.3. 回呼

SQLite 透過呼叫回呼常式來評估 SQL 函數。

2.3.1. 純量函數回呼

純量 SQL 函數由 sqlite3_create_function() 的 xFunc 參數中的單一回呼實作。以下程式碼示範了僅返回其參數的「noop(X)」純量 SQL 函數的實作

static void noopfunc(
  sqlite3_context *context,
  int argc,
  sqlite3_value **argv
){
  assert( argc==1 );
  sqlite3_result_value(context, argv[0]);
}

第一個參數 context 是一個指向不透明物件的指標,該物件描述了呼叫 SQL 函數的上下文。此上下文指標成為函數實作可能呼叫的許多其他常式的第一個參數,包括

`sqlite3_result()` 函數系列用於指定純量 SQL 函數的結果。回呼函數應該呼叫其中一個或多個函數來設定函數的返回值。如果沒有為特定回呼呼叫這些函數,則返回值將為 NULL。

`sqlite3_user_data()` 函數會返回建立 SQL 函數時傳遞給 `sqlite3_create_function()` 的 `pArg` 指標的副本。

`sqlite3_context_db_handle()` 函數返回指向資料庫連線物件的指標。

`sqlite3_aggregate_context()` 函數僅用於實作聚合函數和視窗函數。純量函數不能使用 `sqlite3_aggregate_context()`。`sqlite3_aggregate_context()` 函數包含在介面列表中僅是為了完整性。

純量 SQL 函數實作的第二個和第三個參數 `argc` 和 `argv` 分別是 SQL 函數本身的參數數量和每個參數的值。參數值可以是任何資料類型,因此儲存在 `sqlite3_value` 物件的實例中。可以使用 `sqlite3_value()` 介面系列從這個物件中提取特定的 C 語言值。

2.3.2. 聚合函數回呼

聚合 SQL 函數是透過使用兩個回呼函數 `xStep` 和 `xFinal` 來實作的。`xStep()` 函數會針對聚合的每一列呼叫,而 `xFinal()` 函數會在最後被呼叫以計算最終答案。以下內建 `count()` 函數的(略為簡化)版本說明了這一點:

typedef struct CountCtx CountCtx;
struct CountCtx {
  i64 n;
};
static void countStep(sqlite3_context *context, int argc, sqlite3_value **argv){
  CountCtx *p;
  p = sqlite3_aggregate_context(context, sizeof(*p));
  if( (argc==0 || SQLITE_NULL!=sqlite3_value_type(argv[0])) && p ){
    p->n++;
  }
}   
static void countFinalize(sqlite3_context *context){
  CountCtx *p;
  p = sqlite3_aggregate_context(context, 0);
  sqlite3_result_int64(context, p ? p->n : 0);
}

回想一下,`count()` 聚合函數有兩個版本。沒有參數時,`count()` 返回列數。有一個參數時,`count()` 返回參數不為 NULL 的次數。

`countStep()` 回呼函數會針對聚合中的每一列呼叫一次。如您所見,如果沒有參數,或者如果一個參數不為 NULL,則計數會遞增。

聚合函數的 step 函數應該始終先呼叫 `sqlite3_aggregate_context()` 函數來取得聚合函數的持續狀態。在第一次呼叫 step() 函數時,聚合上下文會初始化為一個大小為 N 位元組的記憶體區塊,其中 N 是 `sqlite3_aggregate_context()` 的第二個參數,並且該記憶體會被歸零。在後續所有對 step() 函數的呼叫中,都會返回相同的記憶體區塊。但是,在記憶體不足的情況下,`sqlite3_aggregate_context()` 可能會返回 NULL,因此聚合函數應該準備好處理這種情況。

所有列處理完畢後,countFinalize() 例行程式會被呼叫一次。此例行程式會計算最終結果,並呼叫 sqlite3_result() 系列函式之一來設定最終結果。聚合上下文會由 SQLite 自動釋放,但在 xFinalize() 例行程式返回之前,必須清理與聚合上下文關聯的任何子結構。如果 xStep() 方法被呼叫一次或多次,則 SQLite 保證即使查詢中止,xFinal() 方法也會被呼叫一次。

2.3.3. 視窗函式回呼

視窗函式 使用與聚合函式相同的 xStep() 和 xFinal() 回呼,以及另外兩個:xValuexInverse。詳情請參閱關於 應用程式定義視窗函式 的文件。

2.3.4. 範例

SQLite 原始碼中散佈著數十個 SQL 函式實作,可用作範例應用程式。內建 SQL 函式使用與應用程式定義的 SQL 函式相同的介面,因此內建函式也可以用作範例。在 SQLite 原始碼中搜尋 "sqlite3_context" 即可找到範例。

3. 安全性影響

如果管理不當,應用程式定義的 SQL 函式可能會成為安全漏洞。例如,假設某個應用程式定義了一個新的 "system(X)" SQL 函式,它將其參數 X 作為命令執行並返回整數結果代碼。其實作可能如下:

static void systemFunc(
  sqlite3_context *context,
  int argc,
  sqlite3_value **argv
){
  const char *zCmd = (const char*)sqlite3_value_text(argv[0]);
  if( zCmd!=0 ){
    int rc = system(zCmd);
    sqlite3_result_int(context, rc);
  }
}

這是一個具有強大副作用的函式。大多數程式設計師會自然地謹慎使用它,但可能不會意識到僅僅是提供它就存在危害。但是,僅僅定義這樣的函式就存在很大的風險,即使應用程式本身從未呼叫它!

假設應用程式在啟動時通常會對 TAB1 表格進行查詢。如果攻擊者可以存取資料庫檔案並修改綱要,如下所示:

ALTER TABLE tab1 RENAME TO tab1_real;
CREATE VIEW tab1 AS SELECT * FROM tab1 WHERE system('rm -rf *') IS NOT NULL;

那麼,當應用程式嘗試打開資料庫、註冊 system() 函式,然後對 "tab1" 表格執行一個無害的查詢時,它反而會刪除其工作目錄中的所有檔案。糟糕!

為了防止這種惡意行為,建立自訂 SQL 函式的應用程式應採取以下一項或多項安全預防措施。採取的預防措施越多越好:

  1. 在每個 資料庫連線 打開後立即呼叫 sqlite3_db_config(db,SQLITE_DBCONFIG_TRUSTED_SCHEMA,0,0)。這可以防止應用程式定義的函式在攻擊者可能通過修改資料庫綱要而偷偷呼叫它們的地方被使用:

    • 在 VIEW 中。
    • 在 TRIGGER 中。
    • 在表格定義的 CHECK 約束中。
    • 在表格定義的 DEFAULT 約束中。
    • 在生成的欄位的定義中。
    • 在表達式索引的表達式部分中。
    • 在部分索引的 WHERE 子句中。

    換句話說,此設定要求應用程式定義的函式只能由應用程式本身呼叫的頂層 SQL 直接執行,而不是作為執行其他看似無害的查詢的結果。

  2. 使用 PRAGMA trusted_schema=OFF SQL 陳述式來停用信任綱要。這與前一點具有相同的效果,但不需要使用 C 語言程式碼,因此可以在使用其他程式語言編寫且無法存取 SQLite C 語言 API 的程式中執行。

  3. 使用 -DSQLITE_TRUSTED_SCHEMA=0 編譯時選項編譯 SQLite。這會使 SQLite 預設不信任綱要內的應用程式定義函式。

  4. 如果任何應用程式自訂的 SQL 函式具有潛在的危險副作用,或者如果它們在被誤用時可能會洩漏敏感資訊給攻擊者,則應使用「enc」參數上的 SQLITE_DIRECTONLY 選項標記這些函式。這表示即使啟用了 trusted-schema 選項,該函式也永遠無法從 schema-code 中運行。

  5. 除非您真的需要,並且仔細檢查了實作並確定即使它落入攻擊者手中也不會造成任何危害,否則永遠不要使用 SQLITE_INNOCUOUS 標記應用程式自訂的 SQL 函式。

本頁面最後修改時間:2024 年 4 月 16 日 17:22:18 (UTC)