在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库的一个怪癖。
答案 0 :(得分:30)
将任何字符串转换为SQLite标识符:
"
替换为""
。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
TEXT
,而非二进制。
SQLITE_MASTER
schema in the FAQ str
,而不是bytes
。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)
第一个要理解的是,表/列名称不能以与转义存储为数据库值的字符串相同的意义转义。
原因是你要么:
理解了这一点,第二个要理解的是,你最终如何“转义”表/列名称取决于你的特定上下文,所以有不止一种方法可以做到这一点,但无论如何,你需要深入了解sqlite中可接受的列/表名是什么或不是。
为了帮助您入门,这里有一个条件:
以“sqlite_”开头的表名保留供内部使用。尝试创建名称以“sqlite _”开头的表是错误的。
更好的是,使用某些列名可能会产生意想不到的副作用:
每个SQLite表的每一行都有一个64位有符号整数键 唯一标识其表中的行。通常是这个整数 叫做“rowid”。可以使用其中一个访问rowid值 与特殊情况无关的名称“rowid”,“oid”或“ rowid ”到位 列名称。如果表包含名为的用户定义列 “rowid”,“oid”或“ rowid ”,那个名字总是指的是 显式声明的列,不能用于检索整数 rowid值。
答案 4 :(得分:6)
从sqlite faq, question 24开始(问题的表述当然没有提供答案,答案可能对你的问题有用):
SQL在包含特殊字符或关键字的标识符(列名或表名)周围使用双引号。所以双引号是一种转义标识符名称的方法。
如果名称本身包含双引号,请将该双引号转义为另一个。
答案 5 :(得分:5)
占位符仅适用于值。列和表名称是结构名称,类似于变量名称;你不能使用占位符来填充它们。
您有三种选择:
答案 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)")
<块引用>
此替换的工作方式与 %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)
);
然后,您只需在执行插入而不是列时动态指定名称。