sqlx::query 不能防 SQL 注入,因其直接拼接字符串会绕过宏的编译期检查与参数绑定机制;必须使用带 ! 后缀的宏(如 query_as!)配合字面量 SQL 和 ?/$1 占位符,并启用 SQLX_OFFLINE=1 及 sqlx prepare 生成 schema 快照,才能实现编译期校验、类型安全与注入防护。

sqlx::query 为什么不能防 SQL 注入
直接拼接字符串传给 sqlx::query(比如 sqlx::query(&format!("SELECT * FROM users WHERE name = '{}'", name)))完全绕过 sqlx 的安全机制。它只是把字符串当普通 SQL 发送给数据库,宏无法介入、参数不会被绑定,和用 std::process::Command 调外部程序一样危险。
真正起作用的是带占位符的写法 + 编译期宏展开检查,而不是函数名本身。
- 必须用
?占位符(PostgreSQL/SQLite)或$1,$2(PostgreSQL)等显式参数位置 - 必须配合
sqlx::query系列宏(sqlx::query,sqlx::query_as,sqlx::query_scalar),不是普通函数调用 - 数据库 URL 必须在编译期可见(如通过
dotenv+sqlx::sqlite::SqlitePool::connect配合SQLX_OFFLINE=1或运行时连接池不触发静态检查)
启用 sqlx 宏的编译期 SQL 检查
sqlx 的宏(如 sqlx::query_as!)会在编译时解析 SQL、校验表名、字段名、参数类型,并绑定到 Rust 类型。但默认不开启——它依赖数据库连接或离线模式生成 schema 信息。
- 开发时:设置环境变量
SQLX_OFFLINE=1,并提前运行sqlx prepare生成.sqlx/目录 - 命令示例:
sqlx prepare --database-url 'sqlite://dev.db' --workspace(需先有可连上的 DB) - CI/部署时:确保
.sqlx/被提交,且SQLX_OFFLINE=1在构建环境中生效 - 若漏掉
SQLX_OFFLINE=1,宏会尝试实时连库——失败则编译报错,但不是注入检查失效,而是根本过不了编译
必须用 _! 后缀宏才能获得静态保障
sqlx::query_as(无感叹号)只是运行时构造查询,不触发任何 SQL 解析;只有 sqlx::query_as!(带感叹号)才是宏,在编译期展开并校验。
常见误用:
// ❌ 不检查 SQL 结构,不防注入,也不校验字段类型
let query = sqlx::query_as("SELECT id, name FROM users WHERE name = $1").bind(name);
<p>// ✅ 编译期确认表存在、字段存在、类型匹配,且参数严格绑定
let user: User = sqlx::query<em>as::<</em>, User>("SELECT id, name FROM users WHERE name = $1")
.bind(name)
.fetch_one(pool)
.await?;
- 所有带
!的宏(query!,query_as!,query_scalar!)才走 AST 解析流程 - 宏内 SQL 必须是字面量字符串(
"..."),不能是&str变量或拼接结果 - 如果字段名写错(如
"SELECT user_name"但表里是name),编译直接失败,不是运行时报错
动态 WHERE 条件怎么写才安全
真实业务常要“按需添加条件”,比如搜索接口支持 name / email / status 组合筛选。不能拼 SQL 字符串,但也不能硬写死所有组合分支。
推荐做法是用 Vec 收集参数 + 构建参数化查询模板:
let mut where_clauses = Vec::new();
let mut params = Vec::new();
<p>if let Some(ref n) = name {
where_clauses.push("name = $1");
params.push(n.as_str());
}
if let Some(ref e) = email {
where_clauses.push("email = $" + &(params.len() + 1).to_string());
params.push(e.as_str());
}</p><p>let where_sql = where_clauses.join(" AND ");
let sql = format!("SELECT * FROM users WHERE {}", where_sql);</p><p>// ⚠️ 注意:这仍不满足宏静态检查!只能用于运行时 query()
// 若坚持静态检查,需为每种常见组合单独写一个 query_as! 调用
更稳妥的静态方案是:对高频组合(如 name+status、email 单独查)分别定义 query_as! 调用,低频或复杂场景再降级用运行时 query() + 人工 review。
容易忽略的一点:即使用了 query_as!,如果数据库 schema 没更新(比如加了新字段但没重跑 sqlx prepare),宏校验可能过期——静态检查依赖的是 schema 快照,不是实时 DB 状态。

















