SQL 注入与参数化查询
本教程共 50 篇 · 第 46 篇 · 更新于 2026-07-31
46. SQL 注入与参数化查询
本节目标:学完本章你能说清楚 SQL 注入是怎么发生的,并能用参数化查询(? 占位符)在任意语言里写出不会被注入的 SQLite 代码。
前面几十章我们一直在安心地写 SELECT、INSERT、UPDATE。但有一个前提一直没明说:这些 SQL 都是我们自己写死的字符串。一旦 SQL 里混入了”用户真实输入的内容”,危险就来了。本章我们就把这块安全短板补上。
先重申一个定位:SQLite 是 serverless(无服务进程)/ 零配置 的嵌入式数据库,数据库就是磁盘上一个单独的文件,你的程序通过库函数直接读写它,背后没有 MySQL、PostgreSQL 那样独立的服务器守护进程。这非常方便,但也意味着安全完全由你自己的代码负责——SQLite 不会替你过滤任何恶意输入,它只会”老实”地把你拼出来的字符串当作 SQL 执行。
一、什么是 SQL 注入
SQL 注入(SQL Injection)指的是:攻击者把”一段 SQL 代码”伪装成普通数据提交给你的程序,而你的程序又恰好把这段输入原样拼进了 SQL 字符串里,于是攻击者的代码被数据库当成了合法指令执行。
打个比方:你写了一张”查用户”的纸条交给数据库,纸条上写着——“帮我找名字叫【这里填用户给的名字】的人”。正常情况下用户填”小明”,纸条变成”帮我找名字叫小明的人”,没问题。但如果用户在框里写的是”小明’; DROP TABLE users;—“,纸条就变成了”帮我找名字叫小明’; DROP TABLE users;— 的人”。数据库根本分不清哪部分是”你的指令”、哪部分是”用户的数据”,它一律照做——于是 users 表被删了。
这就是注入的本质:数据和指令混在一起,数据库无法区分。
二、一个具体的危险例子
假设我们有全局统一的 users 表:
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT,
age INTEGER,
email TEXT
);
现在你想做一个”按名字查用户”的功能,代码里这样拼字符串(这是错误示范,千万不要学):
name = input("请输入用户名:") # 假设用户输入:qa'; DELETE FROM users;--
sql = "SELECT * FROM users WHERE name = '" + name + "';"
# 拼出来的实际 SQL:
# SELECT * FROM users WHERE name = 'qa'; DELETE FROM users;--';
cursor.execute(sql)
注意字符串拼接后,DELETE FROM users 变成了一条独立的 SQL 被一起执行。要注意一个事实:在 sqlite3 命令行以及底层 sqlite3_exec 接口里,SQLite 支持”堆叠查询(stacked queries)“——一条字符串里用分号隔开的多条 SQL 会被依次执行,于是上面拼出的 DELETE FROM users 真的会跑起来。这跟某些”禁止堆叠”的接口不同,是 SQLite 上一个特别需要注意的点。不过也要说清:像 Python sqlite3 的 execute() 通常一次只执行一条语句(多条要改用 executescript),所以同样那段拼好的字符串在 Python 里往往只执行了开头的 SELECT。但别因此侥幸——单条语句内的注入(如让 name = ' OR '1'='1')已足以拖走全表,而换到命令行或别的接口就真会删库。根因永远是你把”用户输入”拼进了 SQL,而不是”能不能堆叠”。
重点提示
SQL 注入不挑数据库。无论你用的是 SQLite、MySQL 还是 PostgreSQL,只要把”用户输入”直接拼进 SQL 字符串,就有被注入的风险。SQLite 在
sqlite3命令行和底层sqlite3_exec里允许堆叠查询,攻击者能借此在一条输入里塞进”删除整张表”的语句;而 Pythonsqlite3这类高级包装通常会限制一次只跑一条语句。但最重要的是:即便不能堆叠,单条语句内的注入(如' OR '1'='1')也足以泄密,所以根因永远是”拼字符串”,不是”能否堆叠”。
三、根治办法:参数化查询
杜绝注入唯一靠谱的办法,是永远不让用户输入变成 SQL 指令的一部分。做法就是:SQL 里该填值的地方用占位符 ? 代替,真正的值通过”参数”单独传给数据库引擎。引擎会负责把参数严格当作”数据”处理,绝不会把它解析成 SQL 语法。
还是上面那个查询,改成参数化就安全了:
name = input("请输入用户名:")
sql = "SELECT * FROM users WHERE name = ?"
cursor.execute(sql, (name,)) # name 永远是"数据",不会再变成指令
这里 ? 是位置占位符。当有多个参数时,按出现顺序一一对应:
SELECT * FROM users WHERE age > ? AND name = ?;
cursor.execute("SELECT * FROM users WHERE age > ? AND name = ?", (18, "小明"))
实用技巧
参数化不只是防注入,还顺手解决了”字符串里的引号要转义”这种麻烦事。比如用户名里本身含有单引号(O’Brien),用
?占位传给引擎,它会自动正确处理,不用你手动加斜杠转义。几乎所有语言的 SQLite 接口都支持?占位符,风格完全一致。
四、为什么”转义”不如”参数化”可靠
有些老资料会教你在拼 SQL 前,先调用 escape 类函数把引号等特殊字符转义。但这属于”自己跟特殊字符斗智斗勇”,容易漏、容易错,而且不同数据库的转义规则还不一样。资料里也明确提醒:不要用 addslashes() 这类通用转义函数来给 SQLite 加引号,它会在取数据时导致奇怪的结果。
参数化的思路则更根本——它从结构上把”指令”和”数据”彻底分开,数据库引擎自己保证数据不会被当成指令。所以记住一句话:能参数化就参数化,不要自己手拼 SQL。
五、占位符的两种风格
除了 ? 这种位置占位符,很多接口(比如 Python 的 sqlite3)还支持命名占位符,用 :名字 表示,参数以字典传递,可读性更好:
cursor.execute(
"INSERT INTO users (name, age, email) VALUES (:n, :a, :e)",
{"n": "小红", "a": 20, "e": "xh@example.com"}
)
两种都安全,选你顺手的即可。关键不在”用哪种占位符”,而在”值必须走参数,绝不走字符串拼接”。
六、类比小结
把数据库想象成一个只会照章办事的助理。你交给它的纸条(SQL)如果自己写好”指令 + 数据”混在一起,助理就分不清轻重;参数化相当于你单独递一张”填好值的表单”(参数),并明确告诉助理”这一栏只是内容、不是命令”。助理自然就不会被伪造的指令带偏。
常见坑
第一,不要把
?占位符外面的字符串再和用户变量拼接——只把”值”放进参数,SQL 模板本身应是写死的常量。第二,SQLite 在命令行/底层接口支持堆叠查询,一次注入可能造成”删表”级破坏,绝不能心存侥幸(但即便不支持堆叠,单条注入也够危险)。第三,即便数据库是本地文件、没有网络暴露,只要程序接收外部输入(配置文件、导入文件、命令行参数、网页表单),就照样需要参数化。第四,当你操作的表之间存在关联(如本教程的users与orders),别忘了 SQLite 默认不强制外键约束——必须显式PRAGMA foreign_keys = ON才生效,且每次新连接都要重新开启(详见第 13、47 章)。参数化管”传值安全”,外键开关管”参照完整性”,两条铁律都要记牢。
最后强调:本章讲的安全原则,和”SQLite 只有 5 种存储类(NULL/INTEGER/REAL/TEXT/BLOB)、没有独立的 DATE 类型”是两条互不相关的知识线——存储类管的是”怎么存数据”,参数化管的是”怎么安全地传数据”,两者都要掌握。在后面第 47、48 章用 Python、Java、Node.js 等语言操作 SQLite 时,请始终把参数化当成铁律。