应用程序定义的 SQL 函数

、执行摘要

使用 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 函数这个家族的每个成员都是一个共同核心的包装。所有家庭成员都完成同样的事情;他们只是有不同的调用签名。

  • sqlite3_create_function() → sqlite3_create_function() 的原始版本允许应用程序创建一个新的 SQL 函数,它可以是标量或聚合。函数的名称使用 UTF8 指定。

  • sqlite3_create_function16() → 此变体的工作方式与 sqlite3_create_function() 原始版本完全相同,只是函数本身的名称被指定为 UTF16 字符串而不是 UTF8 字符串。

  • sqlite3_create_function_v2() → 此变体的工作方式与原始 sqlite3_create_function() 类似,只是它包含一个附加参数,该参数是指向sqlite3_user_data()指针的析构函数的指针,该指针作为所有 sqlite3_create_function() 变体的第 5 个参数传入。当自定义函数被删除时调用该析构函数(如果它是非 NULL)——通常是在数据库连接关闭时。

  • sqlite3_create_window_function() → 此变体与原始 sqlite3_create_function() 类似,只是它接受一组不同的回调指针 -窗口函数定义使用的回调指针。

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 → 第 4 个参数是一个 32 位整数标志,其位传达有关新函数的各种属性。此参数的最初目的是使用以下常量之一指定函数的首选文本编码:

    所有自定义 SQL 函数都将接受任何编码的文本。编码转换将自动发生。首选编码仅指定优化函数实现的编码。可以指定具有相同名称和相同数量参数的多个函数,但用于实现该函数的首选编码和回调不同,SQLite 将选择输入编码与首选编码最匹配的回调集.

    第 4 个参数最近用附加标志位扩展,以传达有关该功能的附加信息。额外的位包括:

    在 SQLite 的未来版本中可能会添加额外的位。

  5. pApp → 第 5 个参数是传递给回调例程的任意指针。SQLite 本身对这个指针不做任何事情,除了让它可用于回调,并在函数未注册时将其传递给析构函数。

2.2. 为同一函数多次调用 sqlite3_create_function()

应用程序为同一个 SQL 函数多次调用 sqlite3_create_function() 是很常见的。例如,如果一个 SQL 函数可以接受 2 个或 3 个参数,那么 sqlite3_create_function() 将为 2 参数版本调用一次,为 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 函数实现的第二个和第三个参数 argcargv是 SQL 函数本身的参数数量和 SQL 函数每个参数的值。参数值可以是任何数据类型,因此存储在sqlite3_value对象的实例中。可以使用sqlite3_value() 接口系列从此对象中提取特定的 C 语言值

2.3.2. 聚合函数回调

聚合 SQL 函数是通过使用两个回调函数xStepxFinal来实现的。为聚合的每一行调用 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,则计数会递增。

聚合的步骤函数应始终以调用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) 这可以防止应用程序定义的函数在攻击者可能能够通过修改数据库模式暗中调用它们的地方使用:

    • 在视图中。
    • 在触发器中。
    • 在表定义的 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 选项,该函数也永远无法从模式代码运行。

  5. 永远不要用SQLITE_INNOCUOUS 标记应用程序定义的 SQL 函数, 除非你真的需要并且你已经仔细检查了实现并且确定它不会造成伤害,即使它落入攻击者的控制之下。