我目前正在用 Python 编写一个应用程序,将其数据存储在 SQLite 数据库中。我希望将数据库文件加密存储在磁盘上,我发现最常见的解决方案是SQLCipher。我将 sqlcipher3 添加到我的项目中以提供 DB-API,并开始使用。使用 SQLCipher,数据库加密密钥以 PRAGMA 语句的形式提供,必须在执行数据库的第一个操作之前提供该语句。
PRAGMA key='hunter2'; -- like this
当我的程序运行时,它会提示用户输入数据库密码。我担心的是,由于这是用户输入的来源,因此它可能容易受到 SQL 注入的攻击。例如,提供密钥的简单方法可能如下所示:
from getpass import getpass
import sqlcipher3
con = sqlcipher3.connect(':memory:')
cur = con.cursor()
password = getpass('Password: ')
cur.execute(f"PRAGMA key='{password}';")
### do stuff with the unencrypted database here
如果有人在密码提示中输入类似“
hunter2'; DROP TABLE secrets;--
”的内容,则替换后生成的 SQL 语句将如下所示:
PRAGMA key='hunter2'; DROP TABLE secrets;--';
通常,解决此问题的方法是使用 DB-API 的参数替换。来自sqlite3文档:
SQL 语句可以使用两种占位符之一:问号(qmark 样式)或命名占位符(命名样式)。对于 qmark 样式,parameters 必须是一个序列,其长度必须与占位符的数量匹配,否则会引发
。对于命名样式,parameters必须是ProgrammingError
(或子类)的实例,它必须包含所有命名参数的键;任何额外的项目都会被忽略。这是两种风格的示例:dict
con = sqlite3.connect(":memory:") cur = con.execute("CREATE TABLE lang(name, first_appeared)") # This is the named style used with executemany(): data = ( {"name": "C", "year": 1972}, {"name": "Fortran", "year": 1957}, {"name": "Python", "year": 1991}, {"name": "Go", "year": 2009}, ) cur.executemany("INSERT INTO lang VALUES(:name, :year)", data) # This is the qmark style used in a SELECT query: params = (1972,) cur.execute("SELECT * FROM lang WHERE first_appeared = ?", params) print(cur.fetchall())
这按照文档示例代码中的预期工作,但是在 PRAGMA 语句中使用占位符时,我们得到一个
OperationalError
告诉我们存在语法错误。两种类型的参数替换都是这种情况。
# these will both fail
cur.execute('PRAGMA key=?;', (password,))
cur.execute('PRAGMA key=:pass;', {'pass': password})
我不知道从这里该去哪里。如果我们实际上在密码提示符下输入恶意字符串,它将不起作用,并产生以下错误:
回溯(最近一次调用最后一次): 文件“”,第 1 行,位于 sqlcipher3.ProgrammingError:您一次只能执行一条语句。
那么早期的“幼稚”代码安全吗?我不确定答案是“是”,只是因为我能想出的 one 恶意字符串不起作用,但似乎没有更好的方法来做到这一点。这里唯一问这个问题的人的答案是我可以找到建议的等效解决方案(python + sqlite 将变量插入 PRAGMA 语句)。我也不想使用 ORM,特别是如果它只是针对这种情况。任何建议将不胜感激,谢谢。
根据“执行中的Python sqlite3字符串变量”的接受的答案,可以使用DB-API替换的地方存在限制:
参数标记只能用于表达式,即值。您不能将它们用于表名和列名等标识符。
看到这一点,我认为
PRAGMA
的参数必须属于与“表和列名称”相同的类别。事实上,我的具体用例是 PRAGMA table_info
,其中参数 is 是表名。
进一步深入研究,我发现Python的
sqlite3
模块依赖于SQLite自己的sqlite3_bind_*
函数来进行参数替换。例如,以下是替换字符串值的代码。我发现进一步证实替换不适用于PRAGMA
参数。
“但是等等,”我想。 “Sam 的参数是键,而不是表名。”如果不深入挖掘,我只能推测这并不重要,而且 SQLite(或 SQLCipher)只是不允许将值绑定到
PRAGMA
语句。
也许您可以通过 SQLCipher 的 C API 而不是通过 SQL 提供密钥?它不能修复我的用例,但可能对你的用例有所帮助!
对于我以及其他任何试图以编程方式向
PRAGMA table_info
提供表名的人来说,我想官方的解决方案是双重和三重检查变量 cannot possible 包含用户输入,验证并转义它。以防万一,交叉手指、脚趾、膝盖和鼻子,然后进行弦替换! 可能会出现什么问题...