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

47 python sqlite

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

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',)]
Run Code Online (Sandbox Code Playgroud)

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

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

sqlite3模块和PEP 249都没有提到转义名称或值的功能.据推测,这是为了阻止用户用字符串组装他们的查询,但这让我感到茫然.

什么函数或技术最适合在SQLite中为列或表使用变量名?我非常希望能够在没有任何其他依赖项的情况下执行此操作,因为我将在我自己的包装器中使用它.

我找了但是找不到SQLite语法相关部分的清晰完整的描述,用来编写我自己的函数.我想确保这适用于SQLite允许的任何标识符,因此试错法对我来说太不确定了.

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

小智 30

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

  • 确保字符串可以编码为UTF-8.
  • 确保字符串不包含任何NUL字符.
  • 更换所有""".
  • 用双引号包裹整个东西.

履行

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("\"", "\"\"") + "\""
Run Code Online (Sandbox Code Playgroud)

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

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

这不会检查保留标识符,因此如果您尝试创建新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
Run Code Online (Sandbox Code Playgroud)

观察和参考

  • SQLite标识符TEXT不是二进制的.
    • SQLITE_MASTER FAQ中的架构
    • 当我给它字节时,Python 2 SQLite API对我大吼大叫它无法解码为文本.
    • Python 3 SQLite API要求查询str不是bytes.
  • SQLite标识符中的双引号被转义为两个双引号.
  • SQLite标识符保留大小写,但它们对ASCII字母不区分大小写.可以启用对unicode感知的不区分大小写.
  • sqlite3可以处理任何其他unicode字符串,只要它可以正确编码为UTF-8.无效的字符串可能导致Python 3.0和Python 3.1.2或其附近的崩溃.Python 2接受了这些无效字符串,但这被认为是一个错误.


Ben*_*Ben 28

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

我不同意那些说"不要使用动态表/列名称,如果你需要你做错了什么"的人.我每天都编写程序来自动化数据库,我会一直这样做.我们有许多包含大量表的数据库,但它们都是基于重复的模式构建的,因此处理它们的通用代码非常有用.每次手写查询都会更容易出错并且更加危险.

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

但是,如果您正在编写通用帮助程序代码来操作数据库中的内容,那么这些注意事项就不适用了.您隐含地允许任何可以调用此类代码的人访问数据库中的所有内容; 这是帮助代码的重点.因此,现在安全问题是确保用户生成的数据永远不会在此类代码中使用.这是编码中的一般安全问题,与盲目exec使用用户输入字符串的问题相同.将插入查询是一个明显的问题,因为您希望能够安全地处理用户输入数据.

所以我的建议是:做任何你想要的动态组装你的查询.使用普通的python字符串模板化表格和列名称,粘贴where子句和连接,所有好的(和可怕的调试)的东西.但要确保你知道这些代码触及的价值必须来自,而不是你的用户[1].然后使用SQLite的参数替换功能将用户输入值安全地作为值插入到查询中.

[1]如果(就像我写的很多代码的情况那样),你的用户完全访问数据库的人,而代码是为了简化他们的工作,那么这种考虑并不真正适用; 您可能正在用户指定的表上组装查询.但是你仍然应该使用SQLite的参数替换来避免最终包含引号或百分号的不可避免的真值.


Sin*_*ion 16

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

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

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

>>> metadata.bind = create_engine('sqlite:///:memory:', echo=True)
Run Code Online (Sandbox Code Playgroud)

然后你可以发出CREATE TABLE ....使用echo=True,sqlalchemy将记录生成的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
>>> 
Run Code Online (Sandbox Code Playgroud)

是的,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
>>> 
Run Code Online (Sandbox Code Playgroud)

并且可以帮助您避免可能的不良情况:

>>> 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
>>> 
Run Code Online (Sandbox Code Playgroud)

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


小智 7

第一个要了解的是,表/列名不能比你可以逃脱存储数据库值的字符串同样的意义进行转义.

原因是你要么:

  • 接受/拒绝潜在的表/列名称,即不保证字符串是可接受的列/表名称,与要存储在某个数据库中的字符串相反; 要么,
  • 清理与创建摘要具有相同效果的字符串:使用的函数是满射的,而不是双射的(再一次,对于要存储在某个数据库中的字符串,逆是正确的); 因此,您不仅无法确定从已清理的名称返回到原始名称,而且还存在无意中尝试创建具有相同名称的两个列或表的风险.

理解了这一点后,要理解的第二件事就是你最终如何"转义"表/列名称取决于你的具体情况,所以有不止一种方法可以做到这一点,但无论如何,你需要挖掘出来确切地知道sqlite中可接受的列/表名是什么或不是什么.

为了帮助您入门,这里有一个条件:

以"sqlite_"开头的表名保留供内部使用.尝试创建名称以"sqlite_"开头的表是错误的.

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

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

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


Ste*_*ven 6

sqlite常见问题解答,问题24(问题的表述当然没有给出答案,答案可能对你的问题有用):

SQL在包含特殊字符或关键字的标识符(列名或表名)周围使用双引号.所以双引号是一种转义标识符名称的方法.

如果名称本身包含双引号,则使用另一个引用双引号.


Eev*_*vee 5

占位符仅用于值.列和表名称是结构名称,类似于变量名称; 你不能使用占位符来填充它们.

你有三个选择:

  1. 在您使用它的任何地方适当地转义/引用列名称.这是脆弱和危险的.
  2. 使用像SQLAlchemy这样的ORM ,它将为您提供转义/引用.
  3. 理想情况下,只是没有动态列名.表和列用于结构 ; 动态的任何东西都是数据,应该在表格而不是其中的一部分.