一、概述
SQLite 3.20.0 (2017-08-01) 添加了三个新的“_pointer()”接口:
邮件列表中很快就出现了关于这些新界面背后的目的、引入它们的原因以及它们解决的问题的问题和困惑。本文试图回答这些问题并澄清困惑。
2. SQLite中指针传递简史
有时,SQLite 扩展可以方便地在子组件之间或扩展与应用程序之间传递非 SQL 值。一些例子:
在FTS3扩展中, MATCH 运算符(执行全文搜索)需要将匹配条目的详细信息传递给snippet()、offsets()和matchinfo()函数,以便这些函数可以转换匹配的详细信息转化为有用的输出。
为了让应用程序 向 FTS5 添加新的扩展,例如新的标记器,应用程序需要一个指向“fts5_api”对象的指针。
在CARRAY 扩展中,应用程序需要告知扩展 C 语言数组的位置,该数组包含扩展实现的表值函数的数据。
传达此信息的传统方式是将 C 语言指针转换为 BLOB 或 64 位整数,然后使用常用接口(如 sqlite3_bind_blob()、sqlite3_result_blob()、sqlite3_value_blob()或整数等价物。
2.1. 提高威胁等级
像传递整数或 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 年春天,一个黑客团队能够使用一长串漏洞利用 root 一台 iMac,其中一个涉及破坏作为 BLOB 值传递给通过 WebSQL 接口运行的 SQLite 数据库的 snippet() FTS3 函数的指针Safari 内部。
我们被告知,在 Android 上,有许多服务会盲目运行任意 SQL,这些 SQL 是由从互联网的不可靠角落下载的不可信应用程序传递给它们的。Android 服务应该更加谨慎地运行来自未经审查的来源的 SQL。笔者没有任何相反的具体例子,但他听说过它们存在的传闻。即使所有 Android 服务都更加小心并适当地审查它们运行的所有 SQL,也很难对它们进行全部审核以验证它们是否安全。因此,有安全意识的人热衷于确保不会通过传递任意 SQL 文本进行攻击。
Fossil版本控制系统(为支持 SQLite 开发 而设计和编写)允许轻度信任的用户输入任意 SQL 以生成故障单报告。该 SQL 使用 sqlite3_set_authorizer()接口进行了清理,并且从未发现任何漏洞。但这是一个潜在的敌对代理能够将任意 SQL 注入系统的示例。
2.2. 防止伪造指针
弥合指针传递安全漏洞的第一次尝试是防止指针值被伪造。这是通过让发送方使用 sqlite3_result_subtype() 将子类型附加到每个指针 并让接收方使用sqlite3_value_subtype()验证该子类型并拒绝具有不正确子类型的指针来实现的。由于无法使用纯 SQL 将子类型附加到结果,因此这可以防止使用 SQL 伪造指针。发送指针的唯一方法是使用 C 代码。如果攻击者可以设置子类型,那么他也可以在不借助 SQLite 的情况下伪造指针。
使用子类型来识别有效指针可以防止 WebSQL 漏洞利用。但事实证明这是一个不完整的解决方案。
2.3. 指针泄漏
在指针上使用子类型可以防止使用纯 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 开发人员(包括这位作者)也想知道同样的事情。但随后安全研究人员指出,指针知识可以帮助攻击者绕过地址空间随机化防御。这称为“指针泄漏”。指针泄漏本身不是漏洞,但它可以帮助攻击者有效地利用其他漏洞。
3.新的指针传递接口
允许扩展组件安全地将私有信息传递给彼此而不引入指针泄漏需要新的接口:
- sqlite3_bind_pointer (S,I,P,T,D) → 将类型 T 的指针 P 绑定到准备语句 S 的第 I 个参数。D 是 P 的可选析构函数。
- sqlite3_result_pointer (C,P,T,D) → 返回类型为 T 的指针 P 作为函数 C 的参数。D 是 P 的可选析构函数。
- sqlite3_value_pointer (V,T) → 返回与值 V 关联的类型 T 的指针,或者如果 V 没有关联的指针,或者如果 V 上的指针的类型不同于 T,则返回 NULL。
对于 SQL,sqlite3_bind_pointer()和 sqlite3_result_pointer()创建的值与 NULL 没有区别。尝试使用hex()函数读取指针值的 SQL 语句将得到 SQL NULL 应答。发现值是否具有关联指针的唯一方法是使用带有适当类型字符串 T 的sqlite3_value_pointer()接口。
sqlite3_value_pointer() 读取的指针值 不能由纯 SQL 生成。因此,SQL 不可能伪造指针。
sqlite3_bind_pointer()和 sqlite3_result_pointer() 生成的指针值不能被纯 SQL 读取。因此,SQL 不可能泄漏指针的值。
通过这种方式,新的指针传递接口似乎解决了与在 SQLite 中将指针值从一个扩展传递到另一个扩展相关的所有安全问题。
3.1. 指针类型
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 扩展发出信号,表明它已传递了一个无效指针。
3.1.1. 指针类型是静态字符串
指针类型是静态字符串,理想情况下应该是直接嵌入 SQLite API 调用中的字符串文字,而不是从其他函数传入的参数。考虑使用整数值作为指针类型,但静态字符串提供了更大的名称空间,从而减少了不相关扩展之间意外类型名称冲突的可能性。
“静态字符串”是指一个以零结尾的字节数组,它在程序的生命周期内是固定不变的。也就是说,指针类型string应该是一个字符串常量。相反,“动态字符串”是一个以零结尾的字节数组,保存在从堆分配的内存中,必须释放它以避免内存泄漏。不要使用动态字符串作为指针类型字符串。
多位评论员表达了对指针类型使用动态字符串的愿望,并希望 SQLite 获得类型字符串的所有权,并在使用完毕后自动释放类型字符串。由于以下原因,该设计被拒绝:
指针类型不是灵活和动态的。指针类型旨在成为设计时常量。应用程序不应在运行时合成指针类型字符串。提供对动态指针类型字符串的支持会导致开发人员通过创建运行时合成指针类型字符串来滥用指针传递接口。要求指针类型字符串是静态的会鼓励开发人员通过在设计时选择固定的指针类型名称并将这些名称编码为常量字符串来做正确的事情。
SQLite 中 SQL 级别的所有字符串值都是动态字符串。要求类型字符串是静态的使得很难创建可以合成任意类型指针的应用程序定义的 SQL 函数。我们不希望用户创建这样的 SQL 函数,因为这样的函数会危及系统的安全性。因此,使用静态字符串的要求有助于保护指针传递接口的完整性免受设计不当的 SQL 函数的影响。静态字符串要求并不是完美的防御,因为老练的程序员可以围绕它进行编码,而新手程序可以简单地解决内存泄漏问题。但是通过声明指针类型字符串必须是静态的,
让 SQLite 获得类型字符串的所有权会对所有应用程序产生性能成本,即使是不使用指针传递接口的应用程序也是如此。SQLite 将值作为sqlite3_value的实例传递目的。该对象有一个析构函数,由于 sqlite3_value 对象几乎用于所有事情,它被频繁调用。如果析构函数需要检查是否有指针类型的字符串需要释放,那么每次调用析构函数时都需要消耗一些额外的 CPU 周期。这些周期加起来。如果指针传递是一种常用的编程范式,我们会愿意承担额外的 CPU 周期成本,但指针传递很少见,因此将运行时成本强加给数十亿个应用程序似乎是不明智的不要仅仅为了一些应用程序的方便而使用指针传递。
如果您觉得在您的应用程序中需要动态指针类型字符串,那么这是一个强烈的迹象,表明您在滥用指针传递接口。您的预期用途可能不安全。请重新考虑您的设计。首先确定您是否真的需要通过 SQL 传递指针。或者可能会找到不同于本文描述的指针传递接口的其他机制。
3.2. 析构函数
sqlite3_bind_pointer()和 sqlite3_result_pointer()例程 的最后一个参数是指向用于在 SQLite 完成 P 指针后处理它的过程的指针。该指针可以为 NULL,在这种情况下不会调用析构函数。
当 D 参数不为 NULL 时,这意味着指针的所有权正在转移到 SQLite。SQLite 将负责在使用完指针后释放与指针关联的资源。如果 D 参数为 NULL,则意味着指针的所有权仍属于调用者,调用者负责处置指针。
请注意,析构函数 D 是针对指针值 P 的,而不是针对类型字符串 T 的。类型字符串 T 应该是一个具有无限生命周期的静态字符串。
如果通过向sqlite3_bind_pointer()或sqlite3_result_pointer() 提供非 NULL D 参数将指针的所有权传递给 SQLite, 则所有权将保留在 SQLite 中,直到对象被销毁。没有办法将所有权从 SQLite 中转移出来并再次返回到应用程序中。
4.指针值的使用限制
使用sqlite3_bind_pointer()、sqlite3_result_pointer()和 sqlite3_value_pointer()接口 搭载 SQL NULL 值的指针 是瞬态和短暂的。指针永远不会写入数据库。指针将无法在排序后存活下来。后一个事实是没有 sqlite3_column_pointer() 接口的原因,因为无法预测查询计划程序是否会在从查询返回值之前插入排序操作,因此无法知道指针值是否通过sqlite3_bind_pointer()或sqlite3_result_pointer()插入到查询 中将一直存在到结果集中。
指针值必须直接从生产者流向消费者,没有中间运算符或函数。指针值的任何转换都会破坏指针并将该值转换为普通的 SQL NULL。
sqlite3_bind_pointer()和sqlite3_result_pointer()接口 的指针和指针类型参数 都由调用者“拥有”。换句话说,调用者负责确保两个值在最后一次通过sqlite3_value_pointer()访问之后保持有效。
5.总结
这篇文章的主要内容:
互联网是一个越来越充满敌意的地方。如今,开发人员应该假设攻击者会找到一种在应用程序中执行任意 SQL 的方法。应设计应用程序以防止任意 SQL 的执行升级为更严重的攻击。
一些 SQLite 扩展受益于传递指针:
- FTS3 MATCH 运算符将指针传递给snippet ()、 offsets()和matchinfo()。
- Carray表值函数需要从应用程序接受指向 C 语言值数组的指针。
- remember() 扩展 需要一个指向 C 语言整数变量的指针,以记住它传递的值 。
- 应用程序需要接收指向“fts5_api”对象的指针,以便向FTS5扩展添加扩展,例如自定义分词器。
绝不应通过将 指针编码为其他 SQL 数据类型(例如整数或 BLOB)来交换指针。相反,使用旨在促进安全指针传递的接口: sqlite3_bind_pointer()、sqlite3_result_pointer()和 sqlite3_value_pointer()。
指针传递的使用是一种高级技术,应不经常使用且谨慎使用。不应随意或粗心地使用指针传递。指针传递是一种锋利的工具,如果使用不当会留下很深的伤痕。
作为每个指针传递接口的最后一个参数的“指针类型”字符串应该是一个独特的、特定于应用程序的字符串文字,直接出现在 API 调用中。指针类型不应该是上层函数传入的参数。