PREPARE语句通过将SQL模板与参数彻底分离来切断SQL注入路径:服务端预编译含$1等占位符的语句并缓存执行计划,后续EXECUTE仅传入参数值,攻击输入如"'; DROP TABLE users; --"始终被当作字符串字面量处理,不参与语法解析。

PREPARE语句如何切断SQL注入路径
PostgreSQL 的 PREPARE 语句本身不是应用层 API,而是数据库服务端的原生命令。它把 SQL 模板(含 $1、$2 等位置参数)一次性发送给服务端解析、生成执行计划并缓存;后续所有 EXECUTE 调用只传入参数值,不传 SQL 结构。这意味着:攻击者输入的 '; DROP TABLE users; -- 永远不会参与语法解析,只会被当作字符串字面量绑定进 WHERE 条件。
为什么 pg_graphql 和 JDBC PreparedStatement 都依赖它
pg_graphql 和 PostgreSQL JDBC 驱动(如 libpq)在底层都调用了服务端的 PREPARE + EXECUTE 协议。它们不拼接 SQL 字符串,而是由驱动将带占位符的语句发往服务端预编译,再把用户输入作为二进制参数值单独传输。这种机制天然隔离了代码与数据——哪怕你传入 name = $1,服务端也只替换值,不重解析语句结构。
哪些场景下 PREPARE 机制会失效
PREPARE 只保护“参数值”,不保护 SQL 结构本身。以下情况它完全不起作用:
- 动态表名或列名:如
SELECT * FROM $1是非法的,PostgreSQL 直接报错ERROR: syntax error at or near "$1" - ORDER BY 或 GROUP BY 字段名:不能用
$1替代created_at,必须白名单校验后拼接 - 条件字段名可变:如
WHERE $1 = $2合法,但WHERE $1 = ?中第一个$1是标识符,无法参数化 - 手动拼接 SQL 字符串后再传给
PREPARE:比如在应用里拼出"SELECT * FROM " + tableVar + " WHERE id = $1",此时tableVar已脱离参数化保护范围
连接层 vs 服务端:PREPARE 的生命周期容易被忽略
PREPARE 创建的语句句柄是服务端资源,绑定到当前数据库会话(session)。如果应用层反复调用 PREPARE my_stmt AS 'SELECT ...' 而不先 DEALLOCATE my_stmt,会堆积无效句柄,最终触发 ERROR: out of shared memory。JDBC 的 PreparedStatement 和 Go 的 *sql.Stmt 在 close 时会自动 DEALLOCATE,但直接在 psql 或函数里手写 PREPARE 就得自己管理生命周期。











