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

时间:2011-06-28 23:33:51

标签: 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',)]

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

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 uses " to quote identifiers但我不确定只是逃避它们就足够了。 PHP的sqlite_escape_string函数文档表明某些二进制数据也可能需要转义,但这可能是PHP库的一个怪癖。

9 个答案:

答案 0 :(得分: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("\"", "\"\"") + "\""

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

  
      
  • '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

观察和参考

  • SQLite标识符为TEXT,而非二进制。
    • SQLITE_MASTER schema in the 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接受了这些无效字符串,但这被认为是一个错误。

答案 1 :(得分:26)

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

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

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

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

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

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

答案 2 :(得分:16)

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

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

foo_table现在代表具有动态架构的表,但您只能在实际数据库连接的上下文中使用它(以便sqlalchemy知道方言,以及做什么用生成的sql)。

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

然后您可以发出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
>>> 

并且是的,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中是完全合法的标识符)

答案 3 :(得分:7)

第一个要理解的是,表/列名称​​不能以与转义存储为数据库值的字符串相同的意义转义

原因是你要么:

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

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

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

  

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

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

  

每个SQLite表的每一行都有一个64位有符号整数键   唯一标识其表中的行。通常是这个整数   叫做“rowid”。可以使用其中一个访问rowid值   与特殊情况无关的名称“rowid”,“oid”或“ rowid ”到位   列名称。如果表包含名为的用户定义列   “rowid”,“oid”或“ rowid ”,那个名字总是指的是   显式声明的列,不能用于检索整数   rowid值。

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

答案 4 :(得分:6)

sqlite faq, question 24开始(问题的表述当然没有提供答案,答案可能对你的问题有用):

  

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

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

答案 5 :(得分:5)

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

您有三种选择:

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

答案 6 :(得分:2)

我做了一些研究,因为我对当前不安全的答案不满意,我建议使用 sqlite 的内部 printf 函数来做到这一点。它用于转义任何标识符(表名、列表...)并使其安全用于连接。

在python中,应该是这样的(我不是python用户,所以可能会有错误,但逻辑本身是有效的):

table = "bar"
escaped_table = connection.execute("SELECT printf('%w', ?)", (table,)).fetchone()[0]
connection.execute("CREATE TABLE \""+escaped_table+"\" (bar TEXT)")

根据documentation of %w

<块引用>

此替换的工作方式与 %q 类似,不同之处在于它将所有双引号字符 (") 加倍而不是单引号,从而使结果适合在 SQL 语句中与双引号标识符名称一起使用。

%w 替换是 SQLite 的增强功能,在大多数其他 printf() 实现中都没有。

这意味着您也可以使用 %q 对单引号执行相同的操作:

table = "bar"
escaped_table = connection.execute("SELECT printf('%q', ?)", (table,)).fetchone()[0]
connection.execute("CREATE TABLE '"+escaped_table+"' (bar TEXT)")

答案 7 :(得分:1)

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)

答案 8 :(得分:0)

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

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

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