Small. Fast. Reliable.
Choose any three.
应用程序定义的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 →第2个参数是要创建的SQL函数的名称。该名称通常以UTF8表示,只是名称应以sqlite3_create_function16()的本机字节顺序以UTF16表示。

    SQL函数名称的最大长度为255个字节的UTF8。任何尝试使用更长名称创建函数的尝试都会导致SQLITE_MISUSE错误。

    可以使用相同的函数名称多次调用SQL函数创建接口。例如,如果两个调用具有相同的函数编号但参数数量不同,则将注册SQL函数的两个变体,每个变体采用不同的参数数量。
  3. nArg →第3个参数始终是函数接受的参数数量。该值必须是介于-1和SQLITE_MAX_FUNCTION_ARG之间的整数 (默认值:127)。值-1表示SQL函数是可变参数函数,可以接受0到SQLITE_MAX_FUNCTION_ARG之间的任意数量的参数。

  4. eTextRep →第四个参数是一个32位整数标志,其位传达有关新功能的各种属性。该参数的原始目的是使用以下常量之一为函数指定首选的文本编码:

    所有自定义SQL函数将接受任何编码的文本。编码转换将自动发生。首选编码仅指定针对其功能实现进行了优化的编码。可以使用相同的名称和相同数量的参数指定多个函数,但是使用不同的首选编码和用于实现该函数的回调函数,SQLite将选择一组与输入编码最匹配首选回调函数的回调函数。

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

    在将来的SQLite版本中可能会添加其他位。

  5. pApp →第5个参数是传递给回调例程的任意指针。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函数的实现:

静态无效noopfunc(
  sqlite3_context *上下文,
  int argc,
  sqlite3_value ** argv
){
  assert(argc == 1);
  sqlite3_result_value(上下文,argv [0]);
}

第一个参数context是指向不透明对象的指针,该对象描述了从中调用SQL函数的内容。该上下文点成为函数实现可能调用的许多其他例程的第一个参数,包括:

sqlite3_result()系列函数用于指定的标量SQL函数的结果。回调应调用其中的一个或多个来设置函数的返回值。如果没有为特定的回调调用这些例程,则返回值为NULL。

sqlite3_user_data()例程返回的副本PARG 这是给指针sqlite3_create_function()时创建的SQL函数。

所述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;
};
静态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 ++;
  }
}   
静态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作为命令运行并返回整数结果代码。也许实现是这样的:

静态void systemFunc(
  sqlite3_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重命名为tab1_real;
CREATE VIEW tab1 AS SELECT * FROM tab1 WHERE system('rm -rf *')NOT NULL;

然后,当应用程序尝试打开数据库时,注册system()函数,然后对“ tab1”表运行无害的查询,而是删除其工作目录中的所有文件。kes!

为避免这种错误,创建自己的自定义SQL函数的应用程序应采取以下一项或多项安全预防措施。采取的预防措施越多越好:

  1. 打开每个数据库连接后,立即 调用sqlite3_db_config(db,SQLITE_DBCONFIG_TRUSTED_SCHEMA,0,0)。这样可以防止在攻击者可能会通过修改数据库架构秘密调用应用程序的地方使用应用程序定义的函数:

    • 在VIEW中。
    • 在TRIGGERSs中。
    • 在CHECK中约束一个表定义。
    • 在默认情况下,表定义的约束。
    • 在生成列的定义中。
    • 在表达式的索引部分中。
    • 在部分索引的WHERE子句中。

    换句话说,此设置要求应用程序定义的函数只能由从应用程序本身调用的顶级SQL直接运行,而不是由于执行其他一些看起来很无辜的查询而导致的。

  2. 使用PRAGMA trust_schema = OFF SQL语句禁用受信任的架构。这具有与前面的项目符号相同的效果,但是不需要使用C代码,因此可以在用另一种编程语言编写的程序中执行,并且这些程序无法访问SQLite C语言API。

  3. 使用-DSQLITE_TRUSTED_SCHEMA = 0编译时选项编译SQLite 。默认情况下,这会使SQLite不信任架构内部的应用程序定义的函数。

  4. 如果任何应用程序定义的SQL函数具有潜在的危险副作用,或者如果滥用它们有可能将敏感信息泄漏给攻击者,则可以使用“ enc”参数上的SQLITE_DIRECTONLY选项标记这些函数 。这意味着即使打开trust-schema选项,也永远无法从模式代码运行该功能。

  5. 除非确实需要,否则请 不要用SQLITE_INNOCUOUS标记应用程序定义的SQL函数,并且您已经仔细检查了实现,并确定即使受到攻击者的控制,它也不会造成任何危害。