dynamic SQL strings should be audited for possible injections
主线位置:查询主线的第一步。
sqlx::query(sql)把sql转成SqlStr时要求impl SqlSafeStr,0.8.6 能编译的query(&format!(...))到这里就过不去。SqlSafeStr的设计和来历见 Query、QueryAs 与 SqlSafeStr,本页只讲升级时碰到它怎么改。
flowchart TB
sql["要执行的 SQL"] --> lit{"字面量吗"}
lit -->|是| ok["直接传"]
lit -->|否| part{"动态的是什么"}
part -->|值| bind["$N 加 bind"]
part -->|"表名列名"| white["白名单加 Assert"]
part -->|"条件或批量"| qb["QueryBuilder"]
本页实验在 M2 Max、macOS、rustc 1.99.0、Postgres 16.14 上运行,依赖 sqlx = "=0.9.0"。
现象
src/bin/sqlsafe_fail.rs,0.8.6 里常见的写法:
let name = "pen";
let rows = sqlx::query(&format!("select id from item where name = '{name}'"))
.fetch_all(&pool)
.await?;
error[E0277]: dynamic SQL strings should be audited for possible injections
--> src/bin/sqlsafe_fail.rs:7:26
|
7 | let rows = sqlx::query(&format!("select id from item where name = '{name}'"))
| ----------- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ dynamic SQL string
| |
| required by a bound introduced by this call
|
= help: the trait `SqlSafeStr` is not implemented for `&std::string::String`
= note: prefer literal SQL strings with bind parameters or `QueryBuilder` to add dynamic data to a query.
To bypass this error, manually audit for potential injection vulnerabilities and wrap with `AssertSqlSafe()`.
For details, see the docs for `SqlSafeStr`.
= note: this trait is only implemented for `&'static str`, not all `&str` like the compiler error may suggest
help: the trait `SqlSafeStr` is implemented for `&str`
--> ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sqlx-core-0.9.0/src/sql_str.rs:52:1
|
52 | impl SqlSafeStr for &'static str {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: required by a bound in `sqlx::query`
--> ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sqlx-core-0.9.0/src/query.rs:653:32
|
653 | pub fn query<'a, DB>(sql: impl SqlSafeStr) -> Query<'a, DB, <DB as Database>::Arguments>
| ^^^^^^^^^^ required by this bound in `query`
For more information about this error, try `rustc --explain E0277`.
另一种写法没有这么友好的提示。把拼好的字符串用 as_str() 交给 Executor::execute(src/bin/sqlsafe_fail2.rs):
let sql = format!("update item set note = null where name = '{name}'");
pool.execute(sql.as_str()).await?;
error[E0597]: `sql` does not live long enough
--> src/bin/sqlsafe_fail2.rs:8:16
|
7 | let sql = format!("update item set note = null where name = '{name}'");
| --- binding `sql` declared here
8 | pool.execute(sql.as_str()).await?;
| -------------^^^----------
| | |
| | borrowed value does not live long enough
| argument requires that `sql` is borrowed for `'static`
9 | Ok(())
10 | }
| - `sql` dropped here while still borrowed
|
note: requirement that the value outlives `'static` introduced here
--> ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sqlx-core-0.9.0/src/executor.rs:63:17
|
63 | E: 'q + Execute<'q, Self::Database>,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
For more information about this error, try `rustc --explain E0597`.
这是同一个原因换了一种报法:&str 在类型上满足 SqlSafeStr(实现是 &'static str),于是类型检查通过,到借用检查时才发现它不是 'static,on_unimplemented 的提示没有机会出现。看到「borrowed for 'static」指向 sqlx 的某个函数,多半就是这个坑。
出处
SqlSafeStr 上的 #[diagnostic::on_unimplemented] 给出了标题、dynamic SQL string 标签和两条 note。实现它的只有 &'static str、AssertSqlSafe<T>(T 是 &str、String、Box<str>、Arc<str>、Arc<String>、Cow<'static, str>)和 SqlStr 自己。收它的入口:query、query_as、query_scalar、raw_sql,以及把字符串直接交给 Executor 时用的 impl Execute for T where T: SqlSafeStr。
AssertSqlSafe 是一个什么都不做的 newtype。文档写得很直白:「SQLx does not provide any sort of sanitization」。它不转义、不检查,只是让「把动态字符串当 SQL 执行」在代码里留下一个能搜到的词。
修法
最省事的改法是把 format! 包进 AssertSqlSafe,能编译,但注入还在。src/bin/sqlsafe_fix.rs 用一个恶意的 name 比较几种改法:
let name = "x' or '1'='1";
// what the format! version would have done once wrapped in AssertSqlSafe
let ids: Vec<i64> = sqlx::query_scalar(AssertSqlSafe(format!("select id from item where name = '{name}'")))
.fetch_all(&pool)
.await?;
println!("format!+Assert -> {ids:?}");
// 1. values: bind
let ids: Vec<i64> = sqlx::query_scalar("select id from item where name = $1")
.bind(name)
.fetch_all(&pool)
.await?;
println!("bind -> {ids:?}");
// 2. identifiers: pick from a fixed list, then assert
let sort = "price; drop table item";
let column = match sort {
"name" | "price" | "created_at" => sort,
_ => "id",
};
let ids: Vec<i64> = sqlx::query_scalar(AssertSqlSafe(format!("select id from item order by {column} desc")))
.fetch_all(&pool)
.await?;
println!("whitelist -> order by {column}: {ids:?}");
// 3. optional filters: QueryBuilder
let min_price: Option<f64> = Some(5.0);
let has_note: Option<bool> = None;
let mut qb: QueryBuilder<Postgres> = QueryBuilder::new("select id from item where true");
if let Some(p) = min_price {
qb.push(" and price >= ").push_bind(p).push("::numeric");
}
if let Some(n) = has_note {
qb.push(" and (note is not null) = ").push_bind(n);
}
println!("QueryBuilder -> {}", qb.sql().as_str());
let ids = qb.build_query_scalar::<i64>().fetch_all(&pool).await?;
println!(" -> {ids:?}");
format!+Assert -> [1, 2, 3]
bind -> []
whitelist -> order by id: [3, 2, 1]
QueryBuilder -> select id from item where true and price >= $1::numeric
-> [1]
- 第一行:
AssertSqlSafe(format!(...))把三行全部返回了,' or '1'='1成了 SQL 的一部分。这正是 0.9 想拦住的写法,换个包装不算修好。 - 值用
$N加bind:值走Bind消息,不进 SQL 文本,恶意字符串只是一个查不到的名字。能用bind的地方都用bind,SQL 保持字面量,AssertSqlSafe根本不用出现。 - 表名、列名、
ORDER BY方向不能做参数,只能拼进文本。从一个固定的集合里选,选不中就用默认值,再包AssertSqlSafe。拼进去的只可能是代码里写死的几个字面量之一。 - 结构是动态的(可选条件、不定长的值列表、批量插入)用
QueryBuilder:push拼结构,push_bind放值并自动写$N。它的new/push不要求SqlSafeStr,安全靠的是只用push_bind放值。IN列表另有更好的写法,见 operator does not exist: bigint = bigint[]。
还有一种情况是 SQL 来自文件或配置(迁移脚本、运维工具里用户自己写的查询)。这时字符串本来就是 SQL,没有可以拦的东西,直接 AssertSqlSafe(sql),并在旁边写明它从哪来。
怎么确认没中招
- 升级到 0.9 后编译不过的每一处,都按上面的顺序改;不要统一替换成
AssertSqlSafe(...)。 - 搜
AssertSqlSafe:每一处的format!参数要么是从固定集合里选出来的字面量,要么是整数这类不可能带引号的值(sqlx 自己生成保存点语句时就是AssertSqlSafe(format!("SAVEPOINT _sqlx_savepoint_{depth}"))),要么是注明来源的整段 SQL。 - 搜
QueryBuilder的push(:参数里不应该出现用户输入。
小结
- 0.9 的
query系列和raw_sql只收&'static str、AssertSqlSafe<T>和SqlStr。format!出来的String报 E0277 加这段专门的提示;借来的非'static&str报 E0597「borrowed for'static」。 AssertSqlSafe不转义也不检查,只是一个能被搜到的标记;包住format!的注入仍然有效。- 修法按顺序:值用
bind;标识符从白名单选再AssertSqlSafe;动态结构用QueryBuilder。