如何在Python中为SQLite表/列名转义string?

在SQLite查询中使用variables值的标准方法是“问号风格”,如下所示:

import sqlite3 with sqlite3.connect(":memory:") as connection: connection.execute("CREATE TABLE foo(bar)") connection.execute("INSERT INTO foo(bar) VALUES (?)", ("cow",)) print(list(connection.execute("SELECT * from foo"))) # prints [(u'cow',)] 

但是,这只适用于将值代入查询。 用于表名或列名时失败:

 import sqlite3 with sqlite3.connect(":memory:") as connection: connection.execute("CREATE TABLE foo(?)", ("bar",)) # raises sqlite3.OperationalError: near "?": syntax error 

sqlite3模块和PEP 249都没有提到转义名称或值的函数。 大概这是为了阻止用户用string来组装他们的查询,但是这让我不知所措。

什么函数或技术最适合在SQLite中为列或表使用variables名? 我宁愿能够做到这一点,没有任何其他的依赖,因为我将在我自己的包装使用它。

我查找了一下,但找不到SQLite语法相关部分的清晰完整的描述,用来写我自己的函数。 我想确保这将适用于SQLite所允许的任何标识符,所以对于我来说,一个反复试验的解决scheme是不太确定的。

SQLite 使用"来引用标识符,但是我不确定只是转义它们就足够了。PHP的sqlite_escape_string函数的文档表明某些二进制数据也可能需要被转义,但这可能是PHP库的怪癖。

将任何string转换为SQLite标识符:

  • 确保string可以编码为UTF-8。
  • 确保string不包含任何NUL字符。
  • 全部replace""
  • 把整个东西用双引号包起来。

履行

 import codecs def quote_identifier(s, errors="strict"): encodable = s.encode("utf-8", errors).decode("utf-8") nul_index = encodable.find("\x00") if nul_index >= 0: error = UnicodeEncodeError("NUL-terminated utf-8", encodable, nul_index, nul_index + 1, "NUL not allowed") error_handler = codecs.lookup_error(errors) replacement, _ = error_handler(error) encodable = encodable.replace("\x00", replacement) return "\"" + encodable.replace("\"", "\"\"") + "\"" 

给定一个string单个参数,它将转义并正确引用或引发exception。 第二个参数可以用来指定在codecs模块中注册的任何error handling程序。 内置的是:

  • 'strict' :在出现编码错误的情况下引发exception
  • 'replace' :用合适的replace标记replace格式错误的数据,如'?''\ufffd'
  • 'ignore' :忽略格式错误的数据并继续,恕不另行通知
  • 'xmlcharrefreplace' :用相应的XML字符引用replace(仅用于编码)
  • 'backslashreplace' :replace为反斜杠转义序列(仅用于编码)

这不检查保留的标识符,所以如果你尝试创build一个新的SQLITE_MASTER表,它不会阻止你。

用法示例

 import sqlite3 def test_identifier(identifier): "Tests an identifier to ensure it's handled properly." with sqlite3.connect(":memory:") as c: c.execute("CREATE TABLE " + quote_identifier(identifier) + " (foo)") assert identifier == c.execute("SELECT name FROM SQLITE_MASTER").fetchone()[0] test_identifier("'Héllo?'\\\n\r\t\"Hello!\" -☃") # works test_identifier("北方话") # works test_identifier(chr(0x20000)) # works print(quote_identifier("Fo\x00o!", "replace")) # prints "Fo?o!" print(quote_identifier("Fo\x00o!", "ignore")) # prints "Foo!" print(quote_identifier("Fo\x00o!")) # raises UnicodeEncodeError print(quote_identifier(chr(0xD800))) # raises UnicodeEncodeError 

观察和参考

  • SQLite标识符是TEXT ,而不是二进制。
    • FAQ中的SQLITE_MASTER模式
    • Python 2的SQLite的API大喊,当我给它的字节,它不能解码为文本。
    • Python 3 SQLite API要求查询是str ,而不是bytes
  • SQLite标识符使用双引号引起来。
    • SQL如SQLite所理解的
  • SQLite标识符中的双引号将被转义为两个双引号。
  • SQLite标识符保留大小写,但对ASCII字母不区分大小写。 可以启用unicode意识不区分大小写。
    • SQLite常见问题问题#18
  • SQLite不支持string或标识符中的NUL字符 。
    • SQLite Ticket 57c971fc74
  • sqlite3可以处理任何其他的Unicodestring,只要它可以正确编码为UTF-8。 无效的string可能会导致Python 3.0和Python 3.1.2之间的崩溃。 Python 2接受这些无效的string,但这被认为是一个错误。
    • Python问题#12569
    • 模块/ _sqlite / cursor.c
    • 我testing了一堆。

psycopg2文档明确build议使用普通的python%或{}格式replace表名和列名(或其他dynamic语法位),然后使用参数机制将值replace为查询。

我不同意大家谁说:“永远不要使用dynamic表/列名,你做错了,如果你需要”。 我每天写程序来自动化数据库的东西,而且我一直都在做。 我们有很多数据库有很多表,但是它们都是build立在重复模式之上的,所以处理它们的通用代码是非常有用的。 每次手写查询都会更加容易出错和危险。

这归结于“安全”的含义。 传统的看法是,使用普通的Pythonstring操作将值放入查询中并不“安全”。 这是因为如果你这样做的话,有各种各样的东西可能会出错,这样的数据往往来自用户,而不是你的控制。 您需要100%可靠的方式正确地转义这些值,以便用户无法将SQL注入到数据值中并让数据库执行它。 所以图书馆作家做这个工作; 你永远不应该。

但是,如果您正在编写通用的帮助程序代码来处理数据库中的事情,那么这些注意事项并不适用。 您隐式地给任何可以调用这样的代码的人访问数据库中的所有内容; 这就是帮手代码的要点 。 所以现在安全问题是确保用户生成的数据永远不能用于这样的代码。 这在编码方面是一个普遍的安全问题,与盲目exec用户input的string一样是一个问题。 将插入到查询中是一个明显的问题,因为您希望能够安全地处理用户input的数据。

所以我的build议是:做任何你想dynamic组装你的查询。 使用正常的Pythonstring模板来分表和列的名称,粘在where子句和连接,所有的好东西(和可怕的debugging)的东西。 但是请确保你知道,这些代码触及的任何值都必须来自 ,而不是你的用户[1]。 然后,您使用SQLite的参数replacefunction来安全地将用户input值作为值插入到查询中。

[1]如果(就像我写的很多代码那样)你的用户那些可以完全访问数据库的人,代码是简化他们的工作,那么这个考虑并不适用。 您可能正在对用户指定的表进行查询。 但是你仍然应该使用SQLite的参数replace来避免最终包含引号或百分号的不可避免的真实值。

如果您确定需要dynamic指定列名称,则应该使用可以安全执行的库(并且抱怨错误的内容)。 SQLAlchemy非常擅长。

 >>> import sqlalchemy >>> from sqlalchemy import * >>> metadata = MetaData() >>> dynamic_column = "cow" >>> foo_table = Table('foo', metadata, ... Column(dynamic_column, Integer)) >>> 

foo_table现在用dynamic模式表示表,但是只能在实际数据库连接的上下文中使用它(以便sqlalchemy知道方言,以及如何处理生成的sql)。

 >>> metadata.bind = create_engine('sqlite:///:memory:', echo=True) 

然后您可以发出CREATE TABLE ... echo=True ,sqlalchemy会logging生成的sql,但是一般情况下,sqlalchemy会自动将生成的sql保存在您的手中(以免考虑将其用于恶意目的)。

 >>> foo_table.create() 2011-06-28 21:54:54,040 INFO sqlalchemy.engine.base.Engine.0x...2f4c CREATE TABLE foo ( cow INTEGER ) 2011-06-28 21:54:54,040 INFO sqlalchemy.engine.base.Engine.0x...2f4c () 2011-06-28 21:54:54,041 INFO sqlalchemy.engine.base.Engine.0x...2f4c COMMIT >>> 

是的,sqlalchemy会照顾任何需要特殊处理的列名,比如列名是sql保留字

 >>> dynamic_column = "order" >>> metadata = MetaData() >>> foo_table = Table('foo', metadata, ... Column(dynamic_column, Integer)) >>> metadata.bind = create_engine('sqlite:///:memory:', echo=True) >>> foo_table.create() 2011-06-28 22:00:56,267 INFO sqlalchemy.engine.base.Engine.0x...aa8c CREATE TABLE foo ( "order" INTEGER ) 2011-06-28 22:00:56,267 INFO sqlalchemy.engine.base.Engine.0x...aa8c () 2011-06-28 22:00:56,268 INFO sqlalchemy.engine.base.Engine.0x...aa8c COMMIT >>> 

并可以避免可能的不良情况:

 >>> dynamic_column = "); drop table users; -- the evil bobby tables!" >>> metadata = MetaData() >>> foo_table = Table('foo', metadata, ... Column(dynamic_column, Integer)) >>> metadata.bind = create_engine('sqlite:///:memory:', echo=True) >>> foo_table.create() 2011-06-28 22:04:22,051 INFO sqlalchemy.engine.base.Engine.0x...05ec CREATE TABLE foo ( "); drop table users; -- the evil bobby tables!" INTEGER ) 2011-06-28 22:04:22,051 INFO sqlalchemy.engine.base.Engine.0x...05ec () 2011-06-28 22:04:22,051 INFO sqlalchemy.engine.base.Engine.0x...05ec COMMIT >>> 

(显然,一些奇怪的东西在sqlite中是完全合法的标识符)

首先要理解的是,表/列名称不能以与将数据库值存储的string转义的方式相同。

原因是你要么:

  • 接受/拒绝潜在的表/列名,即不能保证一个string是一个可接受的列/表名,与保存在某个数据库中的string相反; 要么,
  • 清理与创build摘要具有相同效果的string:使用的函数是完全的,而不是双射的 (对于要存储在某个数据库中的string,反过来也是如此)。 所以你不但不能确定从清理过的名字回到原来的名字,而且还有可能无意中创build两个同名的列或表。

理解了这一点后, 第二个要理解的是,如何最终“转义”表/列名取决于具体的上下文,所以有多种方法可以做到这一点,但无论如何,你需要挖掘,以确定什么是或不是在sqlite中可接受的列/表名称。

为了让你开始,这里是一个条件:

以“sqlite_”开头的表名保留供内部使用。 尝试创build名称以“sqlite_”开头的表是错误的。

更好的是,使用某些列名可能会产生意想不到的副作用:

每个SQLite表的每一行都有一个64位有符号的整数键,用于唯一标识其表中的行。 这个整数通常被称为“rowid”。 可以使用特殊大小写无关名称“rowid”,“oid”或“ rowid ”代替列名称来访问rowid值。 如果一个表包含一个名为“rowid”,“oid”或“ rowid ”的用户定义列,那么该名称将始终引用显式声明的列,并且不能用于检索整数rowid值。

这两个引用的文本是从http://www.sqlite.org/lang_createtable.html

从sqlite常见问题,问题24 (当然问题的表述并不能提供答案可能对您的问题有用):

SQL使用包含特殊字符或关键字的标识符(列名或表名)的双引号。 所以双引号是一种逃避标识符名称的方法。

如果名称本身包含双引号,则将其与另一个双引号进行转义。

占位符仅用于值。 列和表名是结构性的,类似于variables名; 你不能使用占位符来填充它们。

你有三个select:

  1. 在使用它的任何地方适当地转义/引用列名称。 这是脆弱和危险的。
  2. 使用一个像SQLAlchemy这样的ORM,它将负责为你转义/引用。
  3. 理想情况下,没有dynamic列名称。 表和列是结构 ; 任何dynamic的数据 ,应该在表中,而不是其中的一部分。

psycopg2版本2.7(2017年2月发布)开始,可以使用psycopg2.sql以安全方式即时生成列名和表名(标识符)。 这里是一个链接到文档的例子: http : //initd.org/psycopg/docs/sql.html 。

所以在你的问题中编写查询的方法是:

 import sqlite3 from psycopg2 import sql with sqlite3.connect(":memory:") as connection: query = sql.SQL("CREATE TABLE {}").format("bar") connection.execute(query) 

如果你发现你需要一个可变的实体名称(relvar或field),那么你可能做错了什么。 另一种模式是使用属性映射,如下所示:

 CREATE TABLE foo_properties( id INTEGER NOT NULL, name VARCHAR NOT NULL, value VARCHAR, PRIMARY KEY(id, name) ); 

然后,您只需在插入而不是列时dynamic指定名称。