SQLite 3.20.0 (2017-08-01) 中添加了三个新的 "_pointer()" 接口
关于这些新接口背后的目的、它们被引入的原因以及它们解决了什么问题,邮件列表上很快就出现了问题和困惑。本文试图回答这些问题并消除困惑。
SQLite 扩展有时需要在子组件之间或扩展与应用程序之间传递非 SQL 值。一些例子
在 FTS3 扩展中,MATCH 运算符(执行全文搜索)需要将匹配项的详细信息传递给 snippet()、offsets() 和 matchinfo() 函数,以便这些函数可以将匹配的详细信息转换为有用的输出。
为了让应用程序 向 FTS5 添加新扩展(例如新的分词器),应用程序需要指向“fts5_api”对象的指针。
在 CARRAY 扩展 中,应用程序需要告诉扩展包含扩展实现的表值函数数据的 C 语言数组的位置。
传递此信息的传统方法是将 C 语言指针转换为 BLOB 或 64 位整数,然后使用 sqlite3_bind_blob()、sqlite3_result_blob()、sqlite3_value_blob() 或整数等效项之类的常用接口将该 BLOB 或整数通过 SQLite 传递。
将指针像整数或 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 的大多数用法都对上述攻击免疫。但有一些值得注意的例外。即
WebSQL 界面到 webkit 允许任何网页在 Chrome 和 Safari 的浏览器中运行任意 SQL。该任意 SQL 应该在一个沙箱内运行,即使被利用也不能造成任何损害,但事实证明该沙箱的安全性不如人们想象的那么高。在 2017 年春季,一个黑客团队能够使用一系列漏洞入侵一台 iMac,其中之一涉及破坏作为 BLOB 值传递给通过 WebSQL 界面在 Safari 内运行的 SQLite 数据库的 snippet() FTS3 函数的指针。
据称,在 Android 上,许多服务会盲目运行来自互联网可疑角落下载的不受信任的应用程序传递给他们的任意 SQL。Android 服务应该在运行来自未经验证的来源的 SQL 时更加谨慎。作者没有具体的反例,但他听说过有这样的传言。即使所有 Android 服务都更加谨慎,并适当地审查了他们运行的所有 SQL,也难以审核所有服务以验证它们的安全性。因此,安全意识强的人士热衷于确保通过传递任意 SQL 文本不会出现任何漏洞。
Fossil 版本控制系统(旨在支持 SQLite 开发而设计和编写)允许稍微值得信赖的用户输入任意 SQL 来生成故障单报告。该 SQL 使用 sqlite3_set_authorizer() 接口进行清理,并且从未发现任何漏洞。但这是一个潜在的恶意代理能够将任意 SQL 注入系统的例子。
关闭指针传递中安全漏洞的第一个尝试是防止指针值被伪造。这是通过让发送方使用 sqlite3_result_subtype() 将子类型附加到每个指针,并让接收方使用 sqlite3_value_subtype() 验证该子类型,并拒绝具有不正确子类型的指针来实现的。由于没有办法使用纯 SQL 将子类型附加到结果,因此可以防止使用 SQL 伪造指针。发送指针的唯一方法是使用 C 代码。如果攻击者可以设置子类型,那么他也能够在没有 SQLite 的帮助下伪造指针。
使用子类型标识有效指针阻止了 WebSQL 漏洞。但事实证明这只是一个不完整的解决方案。
指针上的子类型的使用阻止了使用纯 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 开发人员(包括作者)也想知道同样的事情。但随后安全研究人员指出,了解指针可以帮助攻击者规避地址空间随机化防御。这被称为“指针泄漏”。指针泄漏本身并不是漏洞,但它可以帮助攻击者有效地利用其他漏洞。
让扩展组件能够安全地相互传递私有信息,并且不会引入指针泄漏,需要新的接口
对于 SQL 而言,由 sqlite3_bind_pointer() 和 sqlite3_result_pointer() 创建的值与 NULL 没有区别。尝试使用 hex() 函数读取指针值的 SQL 语句将获得 SQL NULL 答案。发现值是否与指针相关联的唯一方法是使用 sqlite3_value_pointer() 接口以及适当的类型字符串 T。
由 sqlite3_value_pointer() 读取的指针值不能由纯 SQL 生成。因此,SQL 无法伪造指针。
由 sqlite3_bind_pointer() 和 sqlite3_result_pointer() 生成的指针值不能由纯 SQL 读取。因此,SQL 无法泄漏指针的值。
这样,新的指针传递接口似乎解决了与在 SQLite 中将指针值从一个扩展传递到另一个扩展相关的安全问题。
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 ursor 指针被发送到 carray() 表值函数,而不是它预期的接收者 snippet()。carray() 函数将指针视为指向整数数组的指针,并逐个返回每个整数,从而泄漏 FTS3 ursor 对象的内容。由于 FTS3 ursor 对象包含指向其他对象的指针,因此上面的语句将是一个指针泄漏。
除了,上面的语句不起作用,这要归功于指针类型。由 MATCH 运算符生成的指针具有“fts3cursor”类型,但 carray() 函数期望接收类型为“carray”的指针。由于 sqlite3_result_pointer() 上的指针类型与 sqlite3_value_pointer() 调用上的指针类型不匹配,因此 sqlite3_value_pointer() 在 carray() 中返回 NULL,从而向 CARRAY 扩展发出信号,表明它收到了无效指针。
指针类型是静态字符串,理想情况下应该是直接嵌入到 SQLite API 调用中的字符串文字,而不是从其他函数传递进来的参数。人们曾考虑过使用整数值作为指针类型,但静态字符串提供了更大的名称空间,这减少了无关扩展之间意外类型名称冲突的可能性。
“静态字符串”指的是一个以零结尾的字节数组,它在程序的生命周期中是固定不变的。换句话说,指针类型字符串应该是一个字符串常量。相反,“动态字符串”是指一个以零结尾的字节数组,它存储在从堆中分配的内存中,必须释放以避免内存泄漏。不要使用动态字符串作为指针类型字符串。
多位评论员表达了希望使用动态字符串作为指针类型,并希望 SQLite 拥有类型字符串的所有权,并在使用完类型字符串后自动释放它。我们拒绝了这种设计,原因如下:
指针类型并不旨在灵活和动态。指针类型旨在作为设计时常量。应用程序不应该在运行时合成指针类型字符串。为动态指针类型字符串提供支持会导致开发人员通过创建运行时合成的指针类型字符串来滥用指针传递接口。要求指针类型字符串为静态鼓励开发人员在设计时选择固定的指针类型名称并将这些名称编码为常量字符串,从而做出正确的选择。
SQLite 中 SQL 级别的所有字符串值都是动态字符串。要求类型字符串为静态使得创建可以合成任意类型指针的应用程序定义的 SQL 函数变得困难。我们不希望用户创建此类 SQL 函数,因为此类函数会损害系统的安全性。因此,使用静态字符串的要求有助于防御针对设计不良的 SQL 函数的指针传递接口的完整性。静态字符串要求不是完美的防御,因为熟练的程序员可以绕过它,而新手程序员可以简单地忽略内存泄漏。但通过声明指针类型字符串必须是静态的,我们希望鼓励那些可能使用动态字符串作为指针类型的开发人员更仔细地考虑问题并避免引入安全问题。
让 SQLite 拥有类型字符串的所有权会给所有应用程序带来性能损失,即使是不使用指针传递接口的应用程序也是如此。SQLite 以 sqlite3_value 对象实例的形式传递值。该对象有一个析构函数,由于 sqlite3_value 对象几乎用于所有事物,因此该析构函数会被频繁调用。如果析构函数需要检查是否存在需要释放的指针类型字符串,那么每次调用析构函数时都需要消耗几个额外的 CPU 周期。这些周期加起来很可观。如果指针传递是一种常用的编程范式,我们愿意承受额外 CPU 周期的成本,但指针传递很少见,因此在数十亿计的应用程序中强加运行时成本仅仅是为了方便少数使用指针传递的应用程序似乎是不明智的。
如果您觉得在您的应用程序中需要动态指针类型字符串,这强烈表明您滥用了指针传递接口。您想要的用途可能不安全。请重新考虑您的设计。确定您是否真的需要通过 SQL 传递指针。或者,也许可以找到除了本文介绍的指针传递接口之外的其他机制。
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 传递回应用程序。
使用 sqlite3_bind_pointer()、sqlite3_result_pointer() 和 sqlite3_value_pointer() 接口在 SQL NULL 值上搭载的指针是短暂和转瞬即逝的。这些指针永远不会写入数据库。这些指针不会在排序后存活。后一点解释了为什么没有 sqlite3_column_pointer() 接口,因为无法预测查询规划器是否会在返回查询结果之前插入排序操作,因此无法知道由 sqlite3_bind_pointer() 或 sqlite3_result_pointer() 插入查询中的指针值是否会一直存在到结果集中。
指针值必须直接从其生产者流向其消费者,没有任何中间运算符或函数。对指针值的任何转换都会破坏指针并将值转换为普通的 SQL NULL。
本文的关键要点
互联网正变得越来越充满敌意。如今,开发人员应该假设攻击者会找到一种方法在应用程序中执行任意 SQL 代码。应用程序的设计应防止任意 SQL 代码的执行升级为更严重的漏洞。
一些 SQLite 扩展通过传递指针获益
指针永远不应该通过将它们编码为其他 SQL 数据类型(例如整数或 BLOB)来交换。相反,请使用专门设计用于促进安全指针传递的接口:sqlite3_bind_pointer()、sqlite3_result_pointer() 和 sqlite3_value_pointer()。
指针传递是一种高级技术,应谨慎使用且使用频率较低。不要随意或粗心使用指针传递。指针传递是一把锋利的工具,如果使用不当,会留下深深的伤痕。
作为每个指针传递接口的最后一个参数的“指针类型”字符串应该是一个独特的、特定于应用程序的字符串文字,它直接出现在 API 调用中。指针类型不应该是从更高级别的函数传递进来的参数。