Menu

SQLite 防止 SQL 注入:参数化查询

为什么字符串拼接很危险,SQL 注入到底是怎么发生的,以及 SQLite 的参数化查询如何彻底杜绝它。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

SQL 注入是一种字符串拼接 bug

当用户输入变成数据库所解析的 SQL 文本的一部分时,就会发生 SQL 注入。一旦这条界线变得模糊,用户输入的值变成了数据库要执行的语法,用户就能做你能做的任何事。

下面是经典的反面模式,用的是任何语言都写得出来的伪代码:

-- DON'T DO THIS
query = "SELECT * FROM users WHERE name = '" + user_input + "'"

如果 user_input 是 Ada,你得到的是一次正常的查找。如果 user_input 是 ' OR 1=1 --,你得到的是:

SELECT * FROM users WHERE name = '' OR 1=1 --'

-- 把末尾的引号注释掉了,OR 1=1 匹配了每一行,攻击者刚刚就把你的用户表整个导出去了。更恶劣的版本会用 ; 接上第二条语句,用来删除表、窃取数据,或者插入一个新的管理员账号。

漏洞并不在 SQLite 里,而在构造那个字符串的代码里。

参数化查询:真正的解决办法

参数化查询把 SQL 文本和值分开。SQL 中写的是占位符(? 或 :name),值则单独传入。SQLite 先解析并编译一次 SQL,然后把你的值绑定到编译好的计划中。这些值不可能变成 SQL。

用安全的方式运行一个看起来容易出问题的查找:

在 SQLite 命令行中你是直接输入值的,但在应用代码中,等价的写法是这样的(Python 的 sqlite3 驱动):

# Python - parameterized, safe
cursor.execute("SELECT * FROM users WHERE name = ?", (user_input,))

把 SQL 和值的元组作为两个独立的参数传入。驱动程序会把它们分别发送给 SQLite。即使 user_input 是 ' OR 1=1 --,SQLite 也只会去找名字恰好叫 ' OR 1=1 -- 的用户,结果一个都找不到。

这里的“安全”到底指什么

这种安全不是靠模式匹配或转义实现的,而是由结构决定的。SQLite 在看到你的值之前,就已经把语句编译成了内部形式:

-- The compiled statement has a slot, not a string.
SELECT * FROM users WHERE name = ?
                                 ^
                                 placeholder slot

当你绑定一个值时,它会作为一个带类型的数据(TEXT、INTEGER、BLOB 等等)放进那个槽位。SQLite 永远不会把它重新当作 SQL 解析。攻击者没有可注入的语法,因为解析器早已完成了它的工作。

这就是参数化查询比转义可靠得多的原因。转义试图把危险字符从字符串中清理掉,而绑定从一开始就不会构造出那个危险的字符串。

不要用字符串格式化

每种语言都有一个诱人的捷径:Python 的 f-string、JavaScript 的模板字符串、Java 的 String.format,而对于 SQL 来说,它们每一个都是自找麻烦。

# DON'T - f-string interpolates the value into the SQL text
cursor.execute(f"SELECT * FROM users WHERE name = '{user_input}'")

# DON'T - same problem, % formatting
cursor.execute("SELECT * FROM users WHERE name = '%s'" % user_input)

# DO - placeholder + values argument
cursor.execute("SELECT * FROM users WHERE name = ?", (user_input,))

前两种写法在驱动程序拿到之前,就把用户输入拼进了 SQL 字符串。等 SQLite 收到查询时,伤害已经造成了。第三种写法让 SQL 和值各走各的通道。

这条规则很机械:只要你发现自己在该放值的地方用 +、f-string、format 或模板字符串构造 SQL 字符串,就停下来,改用占位符。

多个参数和命名占位符

真实的查询通常不止一个值。SQLite 同时支持位置占位符 ? 和命名占位符 :name:

在应用代码中,它们对应的写法是:

# Positional
cursor.execute(
    "SELECT * FROM orders WHERE customer = ? AND status = ?",
    ("Ada", "paid"),
)

# Named - clearer when there are several parameters
cursor.execute(
    "SELECT * FROM orders WHERE total > :min_total AND status = :status",
    {"min_total": 50, "status": "paid"},
)

命名参数的扩展性更好。值一旦超过三四个,?, ?, ?, ? 就成了猜谜游戏;而 :customer, :total, :status, :created_at 本身就说明了含义。

标识符需要另一种方法

绑定参数只适用于值:= 右边的内容、IN (...) 里面的内容、VALUES (...) 中的内容。它们不适用于表名、列名,也不适用于 ASC/DESC 这样的 SQL 关键字。

-- This does NOT work. The placeholder can't stand in for a column name.
SELECT * FROM users ORDER BY ? ASC

如果你需要动态的标识符,比如让用户选择按哪一列排序,就要在构造 SQL 之前对照白名单进行校验:

# Allowlist approach
ALLOWED_SORT_COLUMNS = {"name", "created_at", "role"}

if sort_column not in ALLOWED_SORT_COLUMNS:
    raise ValueError(f"Invalid sort column: {sort_column}")

query = f"SELECT * FROM users ORDER BY {sort_column} ASC"
cursor.execute(query)

在用户提供的字符串接近 SQL 之前,先拿它和一组固定的、已知安全的值做比对。这里之所以可以用 f-string,只是因为 sort_column 已经只可能是三个写死的名称之一。

一次真实的注入尝试,如何被化解

我们用一个恶意输入,把两种写法放在一起对比。先建一张小的用户表:

有漏洞的写法会返回所有用户。参数化的写法会去找名字恰好叫 ' OR 1=1 -- 的用户,什么也不返回。同样的输入,完全不同的结果:因为在第二种情况下,值从来没有变成 SQL。

简短清单

  • 对每一个来自代码外部的值使用 ? 或 :name 占位符:用户输入、请求体、环境变量,任何不是你在代码里直接写出来的值。
  • 绝不要在该放值的地方用 +、f-string 或 format 构造 SQL。
  • 对于动态的表名或列名,在拼进查询之前,先对照固定的白名单进行校验。
  • 相信驱动程序。不要自己写引号转义函数。绑定参数的机制历史更久、经过更多实战检验,而且是正确的。
  • 审查团队的查询时只问一个问题:有没有用户输入被拼接进 SQL 文本?如果有,就修掉它。

把这个习惯练成肌肉记忆,SQL 注入就不再是你需要操心的一类 bug 了。

下一步:从应用中连接数据库

你已经看到了查询的安全写法:SQL 中放占位符,值单独传入。下一页会讲解如何在 Python、Node.js 等真实的应用代码中接入 SQLite,包括连接管理,以及参数化查询在典型请求流程中的位置。

常见问题

SQLite 会受到 SQL 注入攻击吗?

会。当应用代码通过拼接字符串来构造查询时,SQLite 和其他任何 SQL 数据库一样容易受到攻击。解决办法不是某个 SQLite 设置,而在于你如何从应用中传递值。使用带 ? 或 :name 占位符的参数化查询,驱动程序就会安全地处理它。

参数化查询是如何防止 SQL 注入的?

当你使用 ? 这样的占位符时,SQLite 会先解析并编译查询,然后把你的值绑定到已编译语句的槽位中。这些值永远不可能变成 SQL 语法:它们只会被当作数据,仅此而已。攻击者根本没有可以突破的字符串。

我能不能只把用户输入中的引号转义一下?

不能。手动转义很脆弱:你总会漏掉某个边界情况(Unicode 引号、编码技巧、注释符号),结果把漏洞发布上线。驱动程序提供 ? 和 :name 参数,就是为了让你不必操心转义。每次都要用它们,哪怕是你“知道”安全的值。

如果表名或列名来自用户输入怎么办?

绑定参数只适用于值,不适用于标识符。如果表名或列名必须是动态的,在把它拼进 SQL 之前,先对照已知名称的白名单进行校验。绝不要通过字符串格式化直接传入用户提供的原始标识符。

Coddy 编程语言插图

和 Coddy 一起学编程

开始