参数化查询能阻止SQL注入,因其在数据库预编译阶段即锁定SQL语法结构,用户输入仅作为纯数据在执行阶段填入内存缓冲区,不参与词法/语法分析;它只保护“值”,不保护表名、列名等结构部分,后者须白名单校验。

参数化查询能阻止SQL注入,是因为它从数据库执行流程的最底层切断了“把用户输入当代码执行”的可能性。不是更谨慎,而是根本没机会解析成代码。
数据库预编译阶段就锁死语法结构
当你调用 cursor.execute("SELECT * FROM users WHERE name = %s", (user_input,))
数据库收到的不是一条完整可执行的 SQL 字符串,而是两件事:第一,一条带占位符的模板(如 SELECT * FROM users WHERE name = $1),被送入解析器生成固定执行计划;第二,user_input 的值作为纯数据,在执行阶段才填进内存缓冲区,不参与任何词法或语法分析。
哪怕 user_input 是 ' OR 1=1 --,数据库也只把它当一个长度为 12 的字符串字面量塞进 $1,不会去闭合引号、不会触发注释、更不会重写 WHERE 条件。
常见错误理解:
- 以为“用了
?或%s就安全”——错,如果这串 SQL 是用f-string拼出来的再传给execute,参数化就完全失效 - 以为“过滤了单引号就没事”——PostgreSQL 支持 Unicode 引号,MySQL 支持反引号绕过,驱动对空字节或编码混淆可能不敏感
参数化只保护“值”,不保护“结构”
表名、列名、ORDER BY 字段、GROUP BY 表达式这些属于 SQL 语法结构的一部分,数据库不允许也不支持对它们做参数绑定。
下面这些写法依然高危:
-
"SELECT * FROM " + table_name + " WHERE id = ?"——table_name是拼接的,?救不了它 -
"ORDER BY " + sort_field + " " + direction—— 必须靠白名单校验sort_field是否在["id", "name", "created_at"]中 - MyBatis 里写
SELECT * FROM user WHERE status = ${status}——${}是字符串替换,不是参数绑定
ORM 的原始查询接口(如 Sequelize 的 query()、Prisma 的 $queryRawUnsafe)也一样:只要用户输入进了 SQL 字符串本体,参数化就形同虚设。
不同语言里参数化是否真正生效,取决于驱动是否走预编译路径
不是所有占位符语法都等价。关键看底层是否把 SQL 模板和参数分两次发给数据库。
- Python 的
psycopg2用%s、sqlite3用?,只要调用的是cursor.execute(sql, params),就走安全路径 - Node.js 的
pg驱动必须用client.query('SELECT ...', [value]);若写成client.query(`SELECT '${value}'`),哪怕 value 经过转义也不保险 - Java 的
PreparedStatement是标准,但若用Statement+String.format拼接,等于裸奔 - PHP 的
PDO::prepare()+execute()安全;mysql_query()(已废弃)或mysqli::query()直接拼接则危险
真正容易被忽略的点是:参数化是否生效,不看你写了什么符号,而看你有没有让驱动把参数单独序列化传输。一旦参数混进 SQL 字符串里,就回到拼接的老路。
最危险的不是不会用参数化,而是误以为用了就万事大吉——尤其是当动态表名、排序字段、搜索通配符(LIKE '%${q}%')混在同一个查询里时,一个没守住,整条链就崩了。

















