libSQL Trusted Schema 安全机制解析:抵御恶意数据库模式注入攻击
【免费下载链接】libsqllibSQL is a fork of SQLite that is both Open Source, and Open Contributions.项目地址: https://gitcode.com/GitHub_Trending/li/libsql
本指南围绕 libSQL(SQLite 的开源分支)中的 Trusted Schema 安全机制展开,说明该机制如何解决"高权限应用读取被攻击者污染过的数据库文件"这一安全隐患。读者将掌握三级函数/虚拟表风险分级模型、PRAGMA trusted_schema与SQLITE_DBCONFIG_TRUSTED_SCHEMA的开关方式、自定义函数与虚拟表的风险标注 API,以及如何借助增强后的function_listPRAGMA 审计数据库中的危险元素。
一、问题背景:数据库模式(Schema)本身可以成为攻击入口
SQLite 数据库文件本质上是一个自包含的容器,其中既存放数据,也存放描述表、视图、触发器、索引、约束等结构的模式定义。这种设计带来了一个安全盲区:如果攻击者能够向数据库文件中注入精心构造的 schema 结构,那么任何以高权限打开该文件的应用,都可能在读取数据时被动执行攻击者植入的代码。
根据 trusted-schema.md 的阐述,攻击者可以在不直接操纵应用逻辑的前提下完成攻击,常见手段包括:
- 将某个表定义替换成视图,让后续查询走攻击者指定的逻辑;
- 向表或视图上附加触发器,在读写发生时触发恶意动作;
- 新增CHECK 约束、生成列(generated columns)、表达式索引、部分索引(partial index),在其表达式或
WHERE子句中嵌入函数调用; - 上述结构一旦调用带有副作用的 SQL 函数或虚拟表,就可能:在高权限受害进程内执行破坏性操作(writefile、删除文件等),或者外泄敏感信息(readfile、读取网络或文件内容后经查询返回)。
Trusted Schema 机制的初衷,就是让高权限应用能够安全地读取可能已被恶意篡改的数据库文件:在打开文件时对 schema 中可执行的元素施加限制,把"攻击者可控的代码路径"压缩到最小。
二、核心模型:Innocuous / Normal / Direct-Only 三级风险分级
机制的基本思路是为每一个 SQL 函数(function)和虚拟表(virtual table)打上一个三级风险标签之一:
| 风险等级 | 含义 | 可用范围 |
|---|---|---|
| Innocuous(无害) | 只能读取本数据库文件中的内容,只能修改本数据库文件 | 任何时间、任何位置都可以安全使用 |
| Direct-Only(仅限直接调用) | 可能产生超出数据库文件边界的副作用(读写外部文件、返回外部信息等) | 只能用于顶层 SQL 语句,禁止出现在触发器、视图、CHECK 约束、DEFAULT 值、生成列、索引表达式、部分索引的 WHERE 子句中 |
| Normal(普通) | 视开关而定 | TRUSTED_SCHEMA=ON时按 Innocuous 对待;TRUSTED_SCHEMA=OFF时按 Direct-Only 对待 |
判定一个函数是否"无害"的标准很朴素:它只能读写自己所在的数据库文件。绝大多数 SQL 函数都属于这一类——例如攻击者即便在 schema 里植入abs()调用,也不会造成任何实际危害,因为abs()只是纯计算。
相反,凡是"副作用发生在数据库文件之外,或从文件之外获取信息"的元素都必须标记为 Direct-Only。文档明确列举了五个典型例子:
fts3_tokenizer()函数writefile()函数readfile()函数zipvfs虚拟表csv虚拟表
这些元素绝不允许被攻击者写进 schema 后诱导高权限应用调用。在 libSQL 源码中,同样思路的内置标注随处可见:例如 dbpage.c 与 dbstat.c 中的系统虚拟表均通过sqlite3_vtab_config(db, SQLITE_VTAB_DIRECTONLY)声明为 Direct-Only,而 json.c 中的 JSON 相关虚拟表则声明为SQLITE_VTAB_INNOCUOUS。
应用自定义元素的默认策略
应用通过sqlite3_create_function()自定义的函数、通过虚拟表模块注册的虚拟表,默认一律进入 Normal 等级,除非应用主动改变其风险等级。
这一设计权衡了安全与兼容:遗留应用可能注册了有风险但尚未标注的函数/虚拟表,若强行默认 Direct-Only 会破坏这些旧应用;而全部默认为 Innocuous 又过于危险。因此采用"默认 Normal + 一条 pragma 一键收紧"的策略——应用只需关闭trusted_schema,所有 Normal 级元素就会立刻按 Direct-Only 行为受到限制。
TEMP 对象例外
限制不适用于 TEMP 数据库。TEMP VIEW 与 TEMP TRIGGER 可以使用任意合法的 SQL 函数或虚拟表,因为 TEMP 对象只能由当前应用进程直接创建,攻击者无法通过污染持久化数据库文件来伪造它们,因此 TEMP 视图与触发器被认为是可信、安全的。
三、开启与关闭 TRUSTED_SCHEMA 的三种方式
方式一:PRAGMA 语句(脚本语言首选)
PRAGMA trusted_schema=(ON|OFF);该 PRAGMA 专为脚本语言或无法直接调用sqlite3_db_config()的二次语言(如 Python、Ruby、Tcl 等)设计,让应用无需写 C 代码即可访问 Trusted Schema 设置。在 pragma.h 中,trusted_schema被声明为一个PragTyp_FLAG类型、参数为SQLITE_TrustedSchema的标志类 pragma——它直接读写数据库连接的SQLITE_TrustedSchema标志位。
方式二:C API(原生应用首选)
sqlite3_db_config(db, SQLITE_DBCONFIG_TRUSTED_SCHEMA, on_or_off, 0);SQLITE_DBCONFIG_TRUSTED_SCHEMA在 sqlite.h.in 中定义,值为1017,在 main.c 中与内部标志SQLITE_TrustedSchema建立映射。该选项关闭后 SQLite 会采取额外的防御步骤,包括:
- 禁止在触发器、视图、CHECK 约束、DEFAULT 子句、表达式索引、部分索引、生成列中使用 SQL 函数,除非该函数被标记为
SQLITE_INNOCUOUS; - 禁止在触发器或视图中使用虚拟表,除非该虚拟表被标记为
SQLITE_VTAB_INNOCUOUS。
完整语义见 sqlite.h.in 的 API 文档。
方式三:编译期默认值(打包发行方)
# 编译时把默认 TRUSTED_SCHEMA 改为 OFF -DSQLITE_TRUSTED_SCHEMA=0-DSQLITE_TRUSTED_SCHEMA=0可以让默认设置为关闭状态。对应逻辑在 main.c 中:#if !defined(SQLITE_TRUSTED_SCHEMA) || SQLITE_TRUSTED_SCHEMA+0!=0决定SQLITE_TrustedSchema标志的初始值,即未定义或非 0 时默认开启。
默认值说明
出于向后兼容考虑,TRUSTED_SCHEMA默认值为ON(开启),这也是 SQLite 官方与 libSQL 的行为基线。但文档明确建议:应用在可行的情况下应主动将其关闭,以获取更强的防御姿态。
四、为自定义函数与虚拟表标注风险等级
4.1 自定义函数:扩展enc参数
sqlite3_create_function()及其同类 API 的enc参数(原本用于指定编码,如SQLITE_UTF8)现在可附加两个新标志:
| 标志 | 作用 |
|---|---|
SQLITE_INNOCUOUS | 将新函数标记为Innocuous,任何场景可用 |
SQLITE_DIRECTONLY | 将新函数标记为Direct-Only,仅限顶层 SQL 调用 |
/* 注册一个纯计算、可安全进入 schema 的函数 */ sqlite3_create_function(db, "my_pure_fn", 1, SQLITE_UTF8 | SQLITE_INNOCUOUS, 0, my_pure_fn_impl, 0, 0); /* 注册一个会读写外部资源的函数,禁止进入 schema/触发器/视图 */ sqlite3_create_function(db, "my_side_effect_fn", 1, SQLITE_UTF8 | SQLITE_DIRECTONLY, 0, my_side_effect_fn_impl, 0, 0);从 sqlite.h.in 的 API 文档可进一步确认二者语义:SQLITE_DIRECTONLY(位掩码0x000080000,见 sqlite.h.in)表示函数只能从顶层 SQL 直接调用,推荐用于所有可能产生副作用的函数;SQLITE_INNOCUOUS(位掩码0x000200000,见 sqlite.h.in)表示函数不太可能被恶意利用,大多数内置函数都带此标志,但除非非常确信,否则不建议为应用自定义函数加SQLITE_INNOCUOUS。
实现细节:在 main.c 中可以看到
SQLITE_FUNC_DIRECT == SQLITE_DIRECTONLY、SQLITE_FUNC_UNSAFE == SQLITE_INNOCUOUS的位级等价断言——API 标志与内核函数标志共用同一组比特位,只是语义方向相反(UNSAFE 表示"不安全",INNOCUOUS 表示"无害",二者是同一比特的两种解读)。
4.2 虚拟表:扩展sqlite3_vtab_config()
虚拟表模块在xConnect中通过sqlite3_vtab_config()声明风险等级:
| 选项 | 作用 |
|---|---|
SQLITE_VTAB_INNOCUOUS | 标记为 Innocuous,可出现在触发器/视图/schema 中 |
SQLITE_VTAB_DIRECTONLY | 标记为 Direct-Only,仅限顶层 SQL 使用 |
static int myvtabConnect(sqlite3 *db, void *pAux, int argc, const char *const *argv, sqlite3_vtab **ppVtab, char **pzErr) { /* 本虚拟表会访问外部文件,必须声明为 Direct-Only */ sqlite3_vtab_config(db, SQLITE_VTAB_DIRECTONLY); /* ... 其余连接逻辑 ... */ }两个选项的常量值定义在 sqlite.h.in:SQLITE_VTAB_INNOCUOUS = 2、SQLITE_VTAB_DIRECTONLY = 3。
4.3 Normal 级与整体收紧
未标注的自定义函数/虚拟表即为Normal。要让它们整体切换为 Direct-Only 行为,只需一条语句:
PRAGMA trusted_schema=OFF;关闭后,所有 Normal 级元素在 schema 相关位置(触发器、视图、约束、生成列、索引表达式等)的使用都会被拒绝,而 Innocuous 级元素仍可正常使用。
五、执行期强制检查:限制是如何落到实处的
5.1 函数检查:sqlite3ExprFunctionUsable
expr.c 中的sqlite3ExprFunctionUsable()是函数侧的执行期校验点。其逻辑用伪代码表示:
如果表达式来自 DDL(EP_FromDDL 属性,即来自 schema 定义): 如果函数带 SQLITE_DIRECTONLY 标志 或者(函数未带 SQLITE_INNOCUOUS 标志 且 SQLITE_TrustedSchema 标志为 0): 报错 "unsafe use of %T()"也就是说,Direct-Only 函数在任何情况下都不允许出现在 schema 结构中;而Normal 函数只有在trusted_schema=OFF时才被拒绝——这正是"Normal 视开关而定"这一规则在字节码生成层的直接体现。
5.2 虚拟表检查:vtabIsReadOnly
delete.c 中的vtabIsReadOnly()处理虚拟表侧的限制。函数注释与逻辑表明,在触发器内部编码语句时:
- 对
SQLITE_VTAB_DIRECTONLY虚拟表的DELETE/INSERT/UPDATE一律禁止; - 对非
SQLITE_VTAB_INNOCUOUS的虚拟表,仅当PRAGMA trusted_schema=ON时才允许写操作。
其判定表达式pTab->u.vtab.p->eVtabRisk > ((pParse->db->flags & SQLITE_TrustedSchema)!=0)将虚拟表的风险等级(Innocuous=1、Normal=2、Direct-Only=3,具体映射见 sqlite.h.in 相关文档)与TrustedSchema标志位比较:等级高于当前信任度即报错"unsafe use of virtual table"。
5.3 攻击链如何被切断
结合上述两点,完整的防护链是:攻击者注入的 schema(视图、触发器、CHECK、生成列、表达式索引、部分索引)在解析阶段即被打上EP_FromDDL标记 → 其中任何非 Innocuous 的函数或虚拟表,在trusted_schema=OFF时都会被编译器拒绝 → 恶意代码根本没有机会进入 VDBE 执行。
六、增强后的function_listPRAGMA:审计工具箱
为配合安全机制,PRAGMA function_list与虚拟表pragma_function_list的输出得到了增强,每个函数现在输出8 列:
| 列名 | 含义 |
|---|---|
name | 函数名 |
builtin | 1 表示内置函数 |
type | 's'=标量,'a'=聚合,'w'=窗口 |
enc | 'utf8'、'utf16le'或'utf16be' |
narg | 参数个数 |
flags | SQLITE_INNOCUOUS、SQLITE_DIRECTONLY、SQLITE_DETERMINISTIC、SQLITE_SUBTYPE、SQLITE_FUNC_INTERNAL的位掩码 |
其中后四列(enc、narg、flags,以及作为第一列补充的name/builtin/type重组)是新增内容。此外,function_list现在会为每个函数列出全部重载条目:例如某函数支持 2 参数与 3 参数两种形式,就会输出两行独立记录。实现位于 pragma.c 的PragTyp_FUNCTION_LIST分支,它同时遍历内置函数哈希表与数据库连接上注册的应用函数,并调用pragmaFunclistLine()逐条输出;标志位掩码的组装见 pragma.c。
审计示例:找出所有禁止进入 schema 的函数
利用新增的flags列,可以查询所有在任何情况下都不允许用于 schema、触发器或视图的 Direct-Only 函数:
SELECT DISTINCT name FROM pragma_function_list WHERE (flags & 0x80000)!=0 ORDER BY name;这里的0x80000正是SQLITE_DIRECTONLY标志位(即十进制的 524288)。以 libSQL 为例,writefile、readfile、fts3_tokenizer等带副作用的函数都会出现在结果中——这为高权限应用在打开数据库前做一次"危险函数白名单预检"提供了标准手段。
虚拟表的审计边界
文档特别指出:虚拟表无法用同样方式静态审计。因为一个虚拟表是 Innocuous、Normal 还是 Direct-Only,取决于xConnect被调用时传入的参数,同样的模块用不同参数实例化后风险等级可能不同。因此,虚拟表的风险判定只能发生在运行期(如 5.2 节的vtabIsReadOnly检查),无法从module_list之类的静态元数据中确定。
七、测试用例验证:行为即规范
仓库中的 test/trustschema1.test 以 Tcl 测试脚本完整验证了上述行为,是最直观的行为规范:
proc f1 {x} {return $x} db function f1 -innocuous -deterministic f1 ;# Innocuous 函数 db function f2 -deterministic f1 ;# Normal 函数 db function f3 -directonly -deterministic f1 ;# Direct-Only 函数 # 1.100:trusted_schema=ON 时,Innocuous 与 Normal 均可进入生成列 CREATE TABLE t1(a, b AS (f1(a+1)), c AS (f2(a+2))); # 1.130:trusted_schema=OFF 后,仅查询 Innocuous 生成列成功 # 1.140:查询到 Normal 函数 f2 的生成列时报错 "unsafe use of f2()" # 1.150:trusted_schema=ON 时 Direct-Only 的 f3 也禁止进入生成列 # 1.160:trusted_schema=OFF 时,TEMP 表中的 f3 依然合法(TEMP 例外)关键用例 1.140 与 1.150 精确验证了本文 5.1 节的判定规则:f2(Normal)在trusted_schema=OFF时被拒,f3(Direct-Only)则无论开关如何都被拒;用例 1.160 验证了 TEMP 对象豁免——即便trusted_schema=OFF,CREATE TEMP TABLE中的生成列使用 Direct-Only 函数f3仍然成功执行。测试文件后半部分还覆盖了 CHECK 约束、索引表达式、视图与触发器中的同类场景。
八、最佳实践与安全建议
综合文档与源码,在 libSQL 项目中落地该机制时建议遵循以下原则:
- 默认关闭(推荐):
TRUSTED_SCHEMA默认开启仅为兼容旧应用;凡是读取来源不可控数据库文件的高权限应用,都应通过PRAGMA trusted_schema=OFF;或sqlite3_db_config(db, SQLITE_DBCONFIG_TRUSTED_SCHEMA, 0, 0)主动关闭。对发行定制版本的团队,可用-DSQLITE_TRUSTED_SCHEMA=0从编译期改变默认值。 - 新注册的函数与虚拟表一律显式标注:纯计算的标
SQLITE_INNOCUOUS/SQLITE_VTAB_INNOCUOUS,任何涉及文件系统、网络、外部进程或敏感信息的标SQLITE_DIRECTONLY/SQLITE_VTAB_DIRECTONLY,不要依赖 Normal 默认值。 - 打开文件前用
function_list审计:借助flags & 0x80000查询预先识别文件 schema 中引用的 Direct-Only 元素,作为纵深防御的第一道闸门。 - 记住 TEMP 豁免是特性而非漏洞:TEMP 视图/触发器完全由应用自身创建,可以放心使用任意函数,不需要为了 TEMP 功能而放开全局
trusted_schema。 - 配合其他防御手段:Trusted Schema 主要防"模式注入 + 函数副作用"类攻击,建议与 defensive 模式、
trusted_schema之外的SQLITE_DBCONFIG_DEFENSIVE等选项配合,形成多层次的只读防护。
附:相关源码与文档索引
- 机制设计文档:libsql-sqlite3/doc/trusted-schema.md
- API 与标志定义:libsql-sqlite3/src/sqlite.h.in、libsql-sqlite3/src/sqlite.h.in#L5618-L5658、libsql-sqlite3/src/sqlite.h.in#L9941-L9972
- db_config 映射与编译期默认值:libsql-sqlite3/src/main.c、libsql-sqlite3/src/main.c#L3548
- PRAGMA 声明与实现:libsql-sqlite3/src/pragma.h、libsql-sqlite3/src/pragma.c
- 执行期检查:libsql-sqlite3/src/expr.c、libsql-sqlite3/src/delete.c
- 内置标注示例:libsql-sqlite3/src/dbpage.c、libsql-sqlite3/src/dbstat.c、libsql-sqlite3/src/json.c
- 行为测试:libsql-sqlite3/test/trustschema1.test
【免费下载链接】libsqllibSQL is a fork of SQLite that is both Open Source, and Open Contributions.项目地址: https://gitcode.com/GitHub_Trending/li/libsql
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考