如何在Python中转义SQLite表/列名称的字符串?

How do you escape strings for SQLite table/column names in Python?

在SQLite查询中使用变量值的标准方法是"问号样式",如下所示:

1
2
3
4
5
6
7
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',)]

但是,这仅适用于将值替换为查询。当用于表名或列名时失败:

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

sqlite3模块和PEP 249均未提及用于转义名称或值的函数。大概是为了阻止用户使用字符串来组合他们的查询,但这使我无所适从。

什么功能或技术最适合在SQLite中为列或表使用变量名?我强烈希望能够在没有任何其他依赖项的情况下做到这一点,因为我将在自己的包装器中使用它。

我在寻找但找不到关于SQLite语法相关部分的清晰完整描述,以用于编写自己的函数。我想确保这对于SQLite允许的任何标识符都适用,因此试错解决方案对我来说还是不确定的。

SQLite使用"引用标识符,但我不确定仅转义标识符就足够了。 PHP的sqlite_escape_string函数文档建议某些二进制数据也可能需要转义,但这可能是PHP库的一个怪癖。


要将任何字符串转换为SQLite标识符:

  • 确保该字符串可以编码为UTF-8。
  • 确保字符串不包含任何NUL字符。
  • 将所有"替换为""
  • 将整个内容用双引号引起来。

实作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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(""","""") +"""

给定一个字符串单个参数,它将转义并正确引用它或引发异常。第二个参数可用于指定在codecs模块中注册的任何错误处理程序。内置的是:

  • 'strict': raise an exception in case of an encoding error
  • 'replace': replace malformed data with a suitable replacement marker, such as '?' or '\ufffd'
  • 'ignore': ignore malformed data and continue without further notice
  • 'xmlcharrefreplace': replace with the appropriate XML character reference (for encoding only)
  • 'backslashreplace': replace with backslashed escape sequences (for encoding only)

这不会检查保留的标识符,因此,如果您尝试创建新的SQLITE_MASTER表,它不会阻止您。

用法示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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?'\\

\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,而不是二进制。

    • 常见问题解答中的SQLITE_MASTER模式
    • 当我给它不能被解码为文本的字节时,Python 2 SQLite API向我吼叫。
    • Python 3 SQLite API要求查询是str,而不是bytes
  • SQLite标识符使用双引号引起来。

    • SQLite理解的SQL
  • SQLite标识符中的双引号以两个双引号转义。
  • SQLite标识符保留大小写,但对ASCII字母不区分大小写。可以启用可识别unicode的大小写。

    • SQLite常见问题#18
  • SQLite不支持字符串或标识符中的NUL字符。

    • SQLite票证57c971fc74
  • sqlite3可以处理任何其他unicode字符串,只要可以将其正确编码为UTF-8。无效的字符串可能会导致Python 3.0和Python 3.1.2或其附近崩溃。 Python 2接受了这些无效的字符串,但这被认为是一个错误。

    • Python问题#12569
    • 模块/_sqlite/cursor.c
    • 我测试了一堆。


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

我不同意每个人所说的"永远不要使用动态表/列名称,如果需要的话,您在做错事"。我每天都编写程序来自动处理数据库中的内容,而且我一直都这样做。我们有很多带有表的数据库,但是它们都是基于重复模式构建的,因此处理它们的通用代码非常有用。每次手写查询都将更容易出错和危险。

归结为"安全"的含义。传统观点认为,使用常规的python字符串操作将值放入查询中并不安全。这是因为如果您这样做,所有事情都会出错,并且此类数据通常来自用户,并且不受您的控制。您需要一种100%可靠的方式来正确转义这些值,以使用户无法在数据值中注入SQL并让数据库执行它。因此,图书馆作家就从事这项工作。你永远不应该。

但是,如果您要编写通用的帮助程序代码以对数据库中的事物进行操作,那么这些注意事项就不会那么有用。您隐式地为任何可以调用此类代码的人提供对数据库中所有内容的访问权限;这就是助手代码的重点。因此,现在安全方面的考虑是确保用户生成的数据永远不会在此类代码中使用。这是编码中的一般安全性问题,与盲目地exec输入用户输入的字符串相同。与在查询中插入值相比,这是一个明显的问题,因为您希望在那里能够安全地处理用户输入的数据。

所以我的建议是:做任何您想动态组合查询的事情。使用普通的python字符串模板来对表和列名称中的sub进行模板化,粘在where子句和联接上,这些都是好东西(而且很容易调试)。但是请确保您知道,此类代码接触的任何值都必须来自您,而不是您的用户[1]。然后,您可以使用SQLite的参数替换功能将用户输入的值作为值安全地插入查询中。

[1]如果(就像我写的很多代码一样)您的用户仍然是完全有权访问数据库的人员,而这些代码是为了简化工作,那么这种考虑就不适用了;您可能正在对用户指定的表组装查询。但是,您仍然应该使用SQLite的参数替换来使自己免受不可避免的真正价值的影响,该价值最终包含引号或百分号。


如果您确定需要动态指定列名,则应该使用可以安全地执行此操作的库(并抱怨错误的地方)。 SQLAlchemy非常擅长于此。

1
2
3
4
5
6
7
>>> import sqlalchemy
>>> from sqlalchemy import *
>>> metadata = MetaData()
>>> dynamic_column ="cow"
>>> foo_table = Table('foo', metadata,
...     Column(dynamic_column, Integer))
>>>

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

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

然后可以发出CREATE TABLE ...。使用echo=True,sqlalchemy会记录生成的sql,但是总的来说,sqlalchemy竭尽全力使生成的sql不受您的控制(除非您考虑将其用于邪恶目的)。

1
2
3
4
5
6
7
8
>>> 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保留字时

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> 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
>>>

并可以避免您遭受不良后果:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> 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中完全是合法的标识符)


首先要了解的是,表/列名的义义与转义存储为数据库值的字符串的义义相同。

原因是您要么必须:

  • 接受/拒绝潜在的表/列名,即不能保证字符串是可接受的列/表名,这与要存储在某些数据库中的字符串相反;要么,
  • 清理将与创建摘要具有相同效果的字符串:所使用的函数是射影,而不是双射的(再次,对于要存储在某些数据库中的字符串,反运算成立);因此,您不仅不能确定从清理过的名称返回原始名称,而且还存在无意间尝试创建两个具有相同名称的列或表的风险。

了解了这一点之后,要了解的第二件事是,如何最终"转义"表/列名取决于您的特定上下文,因此有多种方法可以做到这一点,但是无论如何,您都需要找出在sqlite中确切可接受或不可接受的列/表名称。

要开始使用,这是一个条件:

Table names that begin with"sqlite_" are reserved for internal use. It is an error to attempt to create a table with a name that starts with"sqlite_".

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

Every row of every SQLite table has a 64-bit signed integer key that
uniquely identifies the row within its table. This integer is usually
called the"rowid". The rowid value can be accessed using one of the
special case-independent names"rowid","oid", or"rowid" in place
of a column name. If a table contains a user defined column named
"rowid","oid" or"rowid", then that name always refers the
explicitly declared column and cannot be used to retrieve the integer
rowid value.

引用的两个文本均来自http://www.sqlite.org/lang_createtable.html


从sqlite常见问题24开始(问题的提法当然不能给出答案可能对您的问题有用的线索):

SQL uses double-quotes around identifiers (column or table names) that contains special characters or which are keywords. So double-quotes are a way of escaping identifier names.

如果名称本身包含双引号,请用另一个将双引号转义。


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

您有三种选择:

  • 在您使用的任何地方适当地转义/引用列名。这是脆弱而危险的。
  • 使用SQLAlchemy之类的ORM,它将为您进行转义/引用。
  • 理想情况下,没有动态列名。表和列用于结构;任何动态的东西都是数据,应该在表中而不是表的一部分。

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

    1
    2
    3
    4
    5
    6
    CREATE TABLE foo_properties(
        id INTEGER NOT NULL,
        name VARCHAR NOT NULL,
        value VARCHAR,
        PRIMARY KEY(id, name)
    );

    然后,您只需在执行插入而不是列时动态指定名称即可。