首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Rust 应用防 SQL 注入终极指南:从参数化查询到数据库纵深防御

Rust 应用防 SQL 注入终极指南:从参数化查询到数据库纵深防御

作者头像
不吃草的牛德
发布2026-07-17 21:34:16
发布2026-07-17 21:34:16
170
举报
文章被收录于专栏:RustRust

"Rust 的类型系统帮我消灭了空指针,但它管不住我手贱写出的 format! 拼接 SQL。"

❌ 你是否遇到过?

  • • 觉得用了 Rust 就天然免疫 SQL 注入,安全审查时被打脸?
  • • 动态排序、动态表名场景下,不知道怎么安全拼接 SQL?
  • • 团队里有人用 format! 拼 SQL,review 时还振振有词"参数都是内部传的"?
  • • 不清楚 SQLx 的编译时检查到底防住了什么,没防住什么?
  • • 数据库权限给的是 SUPERUSER,从来没想过最小权限原则?

一、先泼盆冷水:Rust 并不天然防 SQL 注入

很多 Rust 开发者有一种错觉:"Rust 内存安全都搞定了,SQL 注入算什么?"

这是一个危险的认知偏差。

Rust 的类型系统和所有权模型解决的是内存安全问题——悬垂指针、缓冲区溢出、数据竞争。但 SQL 注入的本质是数据与代码的边界混淆:用户输入的数据被当作 SQL 代码执行了。这个问题和编程语言的内存安全模型无关。

来看一个反面教材:

代码语言:javascript
复制
// ❌ 危险!经典 SQL 注入
async fn search_users(pool: &PgPool, keyword: &str) -> Vec<User> {
    let sql = format!("SELECT * FROM users WHERE name LIKE '%{}%'", keyword);
    sqlx::query_as::<_, User>(&sql)
        .fetch_all(pool)
        .await
        .unwrap()
}

这段代码完全可以通过 cargo build,Rust 编译器不会报任何警告。但如果 keyword 传入:

代码语言:javascript
复制
'; DROP TABLE users; --

后果不言而喻。

核心认知:编译器能帮你检查 SQL 语法(如果你用 query! 宏),但无法阻止你用 format! 绕过它。安全意识不能外包给语言特性。


二、SQL 注入攻击面全景

在深入防御之前,先搞清楚攻击面在哪里。Rust Web 应用中,SQL 注入的入口远不止"搜索框"一个:

2.1 常见注入入口

入口

典型场景

风险等级

搜索/过滤

WHERE name LIKE '%{input}%'

🔴 高

排序参数

ORDER BY {user_input}

🟡 中

表名/列名

动态查询不同表 SELECT * FROM {table}

🔴 高

IN 子句

WHERE id IN ({ids})

🔴 高

批量操作

动态构建 INSERT ... VALUES (...), (...)

🟡 中

日志查询

按时间范围动态过滤

🟡 中

API 参数

RESTful 查询参数直接入 SQL

🔴 高

内部调用

微服务间传递的"可信"参数

🟠 低但隐蔽

2.2 一个真实攻击链

假设有一个用户搜索 API:

代码语言:javascript
复制
GET /api/users?name=admin

后端用 format! 拼接 SQL。攻击者构造:

代码语言:javascript
复制
GET /api/users?name=admin'-- 

SQL 变成:

代码语言:javascript
复制
SELECT * FROM users WHERE name = 'admin'-- '

-- 后面全被注释掉。如果原 SQL 后面还有密码校验条件:

代码语言:javascript
复制
SELECT * FROM users WHERE name = 'admin' AND password_hash = '...' 

注入后变成:

代码语言:javascript
复制
SELECT * FROM users WHERE name = 'admin'-- ' AND password_hash = '...'

密码校验被绕过。一条搜索框,撬动整个认证体系。


三、安全查询模式:三大主流方案

3.1 SQLx:参数化查询 + 编译时检查

SQLx 是 Rust 生态中最流行的异步数据库驱动,提供了两层防护。

第一层:参数化查询(运行时防护)
代码语言:javascript
复制
use sqlx::PgPool;

// ✅ 安全:参数化查询,用户输入永远作为数据而非代码
async fn search_users(pool: &PgPool, keyword: &str) -> Result<Vec<User>, sqlx::Error> {
    let pattern = format!("%{}%", keyword);
    sqlx::query_as::<_, User>(
        "SELECT * FROM users WHERE name LIKE $1"
    )
    .bind(pattern)  // $1 被 PostgreSQL 驱动安全转义
    .fetch_all(pool)
    .await
}

原理:参数化查询将 SQL 模板和参数分两次发送给数据库。数据库先编译 SQL 模板,再将参数作为纯数据绑定到占位符。参数中任何 SQL 特殊字符(';--)都会被当作普通字符处理,不可能改变 SQL 语义。

第二层:编译时检查(query! 宏)
代码语言:javascript
复制
// ✅ 更安全:编译时验证 SQL 语法和类型
async fn get_user(pool: &PgPool, id: i64) -> Result<User, sqlx::Error> {
    sqlx::query_as!(
        User,
        "SELECT id, name, email FROM users WHERE id = $1",
        id
    )
    .fetch_one(pool)
    .await
}

query! 宏会在编译时连接数据库(或使用 sqlx prepare 离线模式),验证 SQL 语法正确、表和列存在、参数类型匹配。如果 SQL 写错了,编译直接失败

但注意:query! 宏检查的是静态 SQL 字符串,对动态拼接的 SQL 无能为力:

代码语言:javascript
复制
// ❌ query! 宏也救不了你
let table = get_table_name();  // 运行时决定
let sql = format!("SELECT * FROM {}", table);
sqlx::query_as::<_, User>(&sql)  // 这里用的是 query_as 函数,不是宏
    .fetch_all(pool)
    .await

3.2 Diesel:ORM 级别的类型安全

Diesel 通过 DSL(领域特定语言)构建查询,从根本上避免了字符串拼接:

代码语言:javascript
复制
use diesel::prelude::*;
use diesel::pg::PgConnection;

// Schema 定义
diesel::table! {
    users (id) {
        id -> Int8,
        name -> Varchar,
        email -> Varchar,
        active -> Bool,
    }
}

// ✅ 安全:Diesel DSL 生成参数化 SQL
fn search_users(conn: &mut PgConnection, keyword: &str) -> QueryResult<Vec<User>> {
    use crate::schema::users::dsl::*;
    
    let pattern = format!("%{}%", keyword);
    users
        .filter(name.like(pattern))
        .filter(active.eq(true))
        .limit(50)
        .load::<User>(conn)
}

Diesel 生成的 SQL 是:

代码语言:javascript
复制
SELECT * FROM users WHERE name LIKE $1 AND active = $2 LIMIT $3
-- 参数: ["%keyword%", true, 50]

完全参数化,零注入风险。

但 Diesel 同样提供了 sql_query 用于原生 SQL,这里必须手动确保安全:

代码语言:javascript
复制
// ⚠️ 原生 SQL,需手动参数化
fn raw_search(conn: &mut PgConnection, keyword: &str) -> QueryResult<Vec<User>> {
    let pattern = format!("%{}%", keyword);
    diesel::sql_query("SELECT * FROM users WHERE name LIKE $1")
        .bind::<Text, _>(pattern)  // 必须手动 bind
        .load::<User>(conn)
}

3.3 SeaORM:动态查询的安全构建

SeaORM 基于动态构建器模式,同样天然安全:

代码语言:javascript
复制
use sea_orm::*;

// ✅ 安全:SeaORM Entity 查询
async fn search_users(db: &DatabaseConnection, keyword: &str) -> Result<Vec<user::Model>, DbErr> {
    let pattern = format!("%{}%", keyword);
    user::Entity::find()
        .filter(user::Column::Name.is_not_null())
        .filter(
            Condition::any()
                .add(user::Column::Name.like(&pattern))
                .add(user::Column::Email.like(&pattern))
        )
        .filter(user::Column::Active.eq(true))
        .all(db)
        .await
}

三大方案对比

特性

SQLx

Diesel

SeaORM

参数化查询

✅ 原生支持

✅ DSL 自动生成

✅ 构建器自动生成

编译时检查

✅ query! 宏

✅ Schema 宏

❌ 运行时

动态查询

需手动处理

较灵活

✅ 最灵活

原生 SQL 支持

✅ query()

✅ sql_query()

✅ Statement

学习曲线

中高

选型建议:需要极致性能和 SQL 控制力 → SQLx;需要强类型保障和编译时安全 → Diesel;需要灵活动态查询 → SeaORM。无论选哪个,核心原则不变:永远不要用字符串拼接构建 SQL。


四、动态 SQL 的安全构建

参数化查询解决了值注入,但有些场景参数化搞不定:

  • ORDER BY 后面接的是列名,不是值
  • • 表名/列名是动态的(多租户场景)
  • IN (?, ?, ?) 的占位符数量不确定

这些是 SQL 注入的高发区。下面逐一击破。

4.1 动态排序字段

代码语言:javascript
复制
// ❌ 危险!列名不能参数化
async fn list_users_bad(pool: &PgPool, sort: &str, order: &str) -> Vec<User> {
    let sql = format!("SELECT * FROM users ORDER BY {} {}", sort, order);
    sqlx::query_as::<_, User>(&sql).fetch_all(pool).await.unwrap()
}

// ✅ 安全:白名单校验
async fn list_users_safe(pool: &PgPool, sort: &str, order: &str) -> Result<Vec<User>, AppError> {
    // 1. 白名单定义合法列名
    const ALLOWED_SORT: &[&str] = &["id", "name", "email", "created_at"];
    const ALLOWED_ORDER: &[&str] = &["ASC", "DESC"];
    
    let sort_field = ALLOWED_SORT.iter()
        .find(|&&s| s.eq_ignore_ascii_case(sort))
        .ok_or(AppError::BadRequest("非法排序字段".into()))?;
    
    let order_dir = ALLOWED_ORDER.iter()
        .find(|&&o| o.eq_ignore_ascii_case(order))
        .ok_or(AppError::BadRequest("非法排序方向".into()))?;
    
    // 2. 用 format! 拼接——但内容来自白名单,安全
    let sql = format!("SELECT * FROM users ORDER BY {} {}", sort_field, order_dir);
    
    sqlx::query_as::<_, User>(&sql)
        .fetch_all(pool)
        .await
        .map_err(Into::into)
}

核心原则:列名/表名不能参数化,必须用白名单。永远不要信任用户传入的标识符。

4.2 动态表名(多租户场景)

代码语言:javascript
复制
use std::collections::HashSet;

// ✅ 安全:多租户表名白名单
pub struct TableNameValidator {
    allowed: HashSet<String>,
}

impl TableNameValidator {
    pub fn new(prefix: &str) -> Self {
        // 预加载所有合法的租户表名
        let allowed = (1..=1000)
            .map(|i| format!("{}_tenant_{}", prefix, i))
            .collect();
        Self { allowed }
    }
    
    pub fn validate(&self, table: &str) -> Result<&str, AppError> {
        // 严格白名单校验
        if self.allowed.contains(table) {
            Ok(table)
        } else {
            Err(AppError::BadRequest(format!("非法表名: {}", table)))
        }
    }
}

// 使用
async fn query_tenant_data(
    pool: &PgPool,
    validator: &TableNameValidator,
    tenant_table: &str,
    user_id: i64,
) -> Result<Vec<Record>, AppError> {
    let table = validator.validate(tenant_table)?;
    let sql = format!("SELECT * FROM {} WHERE user_id = $1", table);
    
    sqlx::query_as::<_, Record>(&sql)
        .bind(user_id)  // 值仍然参数化
        .fetch_all(pool)
        .await
        .map_err(Into::into)
}

更好的方案:多租户场景优先使用 PostgreSQL 的 Row Level Security(RLS),而不是物理分表。后面数据库加固部分会讲。

4.3 动态 IN 子句

代码语言:javascript
复制
// ❌ 危险!经典 IN 注入
async fn get_users_bad(pool: &PgPool, ids: &str) -> Vec<User> {
    let sql = format!("SELECT * FROM users WHERE id IN ({})", ids);
    sqlx::query_as::<_, User>(&sql).fetch_all(pool).await.unwrap()
}

// ✅ 方案一:参数化展开
async fn get_users_safe_v1(pool: &PgPool, ids: &[i64]) -> Result<Vec<User>, sqlx::Error> {
    if ids.is_empty() {
        return Ok(vec![]);
    }
    
    // 构建占位符: $1, $2, $3, ...
    let placeholders: Vec<String> = (1..=ids.len())
        .map(|i| format!("${}", i))
        .collect();
    
    let sql = format!(
        "SELECT * FROM users WHERE id IN ({})",
        placeholders.join(", ")
    );
    
    let mut query = sqlx::query_as::<_, User>(&sql);
    for id in ids {
        query = query.bind(id);
    }
    
    query.fetch_all(pool).await
}

// ✅ 方案二:使用 ANY 数组(PostgreSQL 推荐)
async fn get_users_safe_v2(pool: &PgPool, ids: &[i64]) -> Result<Vec<User>, sqlx::Error> {
    sqlx::query_as::<_, User>(
        "SELECT * FROM users WHERE id = ANY($1)"
    )
    .bind(ids)  // 绑定为数组类型
    .fetch_all(pool)
    .await
}

方案二更简洁、更安全、性能也更好(数据库只需解析一个占位符)。

4.4 动态 WHERE 条件组合

代码语言:javascript
复制
use sqlx::QueryBuilder;

// ✅ 安全:使用 QueryBuilder 动态构建
async fn filter_users(
    pool: &PgPool,
    filters: &UserFilters,
) -> Result<Vec<User>, sqlx::Error> {
    let mut builder = QueryBuilder::new("SELECT * FROM users WHERE 1=1");
    
    if let Some(name) = &filters.name {
        builder.push(" AND name LIKE ").push_bind(format!("%{}%", name));
    }
    
    if let Some(email) = &filters.email {
        builder.push(" AND email = ").push_bind(email);
    }
    
    if let Some(min_age) = filters.min_age {
        builder.push(" AND age >= ").push_bind(min_age);
    }
    
    if let Some(max_age) = filters.max_age {
        builder.push(" AND age <= ").push_bind(max_age);
    }
    
    if let Some(active) = filters.active {
        builder.push(" AND active = ").push_bind(active);
    }
    
    // push_bind 自动参数化,安全!
    builder.push(" ORDER BY created_at DESC LIMIT ")
           .push_bind(filters.limit.unwrap_or(50));
    
    builder.build_query_as::<User>()
        .fetch_all(pool)
        .await
}

QueryBuilder 是 SQLx 0.7+ 引入的利器,专门解决动态 SQL 构建问题。push_bind 方法确保所有值都被参数化,push 方法只用于拼接不可变 SQL 片段。规则很简单:凡是来自用户的值,一律走 push_bind


五、六大危险反模式

以下是 code review 中必须拦截的写法:

反模式 1:format! 拼接用户输入

代码语言:javascript
复制
// ❌ 绝对禁止
let sql = format!("SELECT * FROM users WHERE name = '{}'", user_input);

反模式 2:字符串插值

代码语言:javascript
复制
// ❌ 绝对禁止
let sql = format!("SELECT * FROM {table} WHERE id = {id}");

反模式 3:信任"内部"参数

代码语言:javascript
复制
// ❌ 危险:内部 API 也可能被利用
async fn internal_query(pool: &PgPool, table: &str) -> Vec<Record> {
    // 即使 table 来自内部服务,也可能被横向移动的攻击者利用
    let sql = format!("SELECT * FROM {}", table);
    sqlx::query_as::<_, Record>(&sql).fetch_all(pool).await.unwrap()
}

反模式 4:手动转义引号

代码语言:javascript
复制
// ❌ 危险:手动转义不可靠
let escaped = user_input.replace("'", "''");
let sql = format!("SELECT * FROM users WHERE name = '{}'", escaped);

手动转义永远比数据库驱动的参数化差——编码问题、二次注入、多字节字符都可能绕过。

反模式 5:正则过滤"危险字符"

代码语言:javascript
复制
// ❌ 危险:黑名单永远不完整
fn sanitize(input: &str) -> String {
    input.replace("'", "").replace(";", "").replace("--", "")
}

黑名单思维是安全领域的反面教材。攻击者总有你没想到的绕过方式。

反模式 6:日志中泄露 SQL 参数

代码语言:javascript
复制
// ❌ 间接风险:日志泄露敏感数据
tracing::info!("Executing: SELECT * FROM users WHERE email = '{}'", email);

虽然不是注入本身,但日志泄露可能导致攻击者获取有价值的信息辅助注入攻击。


六、数据库纵深防御

应用层的安全查询是第一道防线,但纵深防御要求我们在数据库层再加几道锁。

6.1 最小权限原则

代码语言:javascript
复制
-- ❌ 危险:应用用 superuser 连接
CREATE USER app_user WITH SUPERUSER PASSWORD '...';

-- ✅ 安全:最小权限
-- 1. 创建只读角色
CREATE ROLE app_reader;
GRANT SELECT ON users, posts, comments TO app_reader;

-- 2. 创建读写角色(仅限必要表)
CREATE ROLE app_writer;
GRANT SELECT, INSERT, UPDATE ON users, posts TO app_writer;
-- 注意:不给 DELETE 权限
-- 注意:不给 DROP、ALTER 权限

-- 3. 创建应用专用用户
CREATE USER app_api WITH PASSWORD '...' IN ROLE app_reader;
CREATE USER app_admin WITH PASSWORD '...' IN ROLE app_writer;

-- 4. 撤销 public 权限
REVOKE ALL ON SCHEMA public FROM PUBLIC;
GRANT USAGE ON SCHEMA public TO app_reader, app_writer;

原则:应用连接数据库的账号,权限应刚好够用,不多一分。 即使发生注入,攻击者也只能在权限范围内活动——没有 DROP TABLE、没有 pg_read_file、没有 COPY TO PROGRAM

6.2 使用视图限制数据访问

代码语言:javascript
复制
-- 创建视图,隐藏敏感列
CREATE VIEW user_public AS
SELECT id, name, avatar_url, created_at
FROM users;
-- 不暴露 email、password_hash、phone 等

-- 应用查询视图而非基表
GRANT SELECT ON user_public TO app_reader;
REVOKE SELECT ON users FROM app_reader;
代码语言:javascript
复制
// 应用层查询视图
async fn get_public_users(pool: &PgPool) -> Result<Vec<UserPublic>, sqlx::Error> {
    sqlx::query_as!(
        UserPublic,
        "SELECT id, name, avatar_url, created_at FROM user_public"
    )
    .fetch_all(pool)
    .await
}

6.3 PostgreSQL Row Level Security(RLS)

多租户场景的最佳实践,替代物理分表:

代码语言:javascript
复制
-- 1. 启用 RLS
ALTER TABLE tenant_data ENABLE ROW LEVEL SECURITY;

-- 2. 创建策略:用户只能看到自己租户的数据
CREATE POLICY tenant_isolation ON tenant_data
    USING (tenant_id = current_setting('app.current_tenant_id')::bigint);

-- 3. 应用层设置当前租户 ID
-- SET app.current_tenant_id = '42';
代码语言:javascript
复制
// Rust 应用层设置租户上下文
async fn with_tenant_context(
    pool: &PgPool,
    tenant_id: i64,
    f: impl AsyncFnOnce(&PgPool) -> Result<(), AppError>,
) -> Result<(), AppError> {
    let mut tx = pool.begin().await?;
    
    // 设置会话变量,RLS 策略自动生效
    sqlx::query("SET LOCAL app.current_tenant_id = $1")
        .bind(tenant_id.to_string())
        .execute(&mut *tx)
        .await?;
    
    // 后续所有查询自动被 RLS 过滤
    let result = f(pool).await;
    
    tx.commit().await?;
    result
}

// 使用
with_tenant_context(&pool, tenant_id, async |pool| {
    // 即使 SQL 是 SELECT *,也只会返回当前租户的数据
    let records = sqlx::query_as!(Record, "SELECT * FROM tenant_data")
        .fetch_all(pool)
        .await?;
    Ok(())
}).await?;

RLS 的价值:即使应用层发生注入,攻击者也无法跨租户访问数据——数据库引擎强制执行隔离。

6.4 存储过程封装

对于复杂业务逻辑,用存储过程封装,应用只调用接口:

代码语言:javascript
复制
-- 创建存储过程
CREATE OR REPLACE FUNCTION transfer_funds(
    p_from_id BIGINT,
    p_to_id BIGINT,
    p_amount DECIMAL(18,2)
) RETURNS VOID AS $$
BEGIN
    -- 所有逻辑在数据库内完成,应用层无法注入
    UPDATE accounts SET balance = balance - p_amount WHERE id = p_from_id;
    UPDATE accounts SET balance = balance + p_amount WHERE id = p_to_id;
    INSERT INTO transactions (from_id, to_id, amount) VALUES (p_from_id, p_to_id, p_amount);
END;
$$ LANGUAGE plpgsql;

-- 只授予执行权限,不授予表级权限
GRANT EXECUTE ON FUNCTION transfer_funds TO app_writer;
REVOKE ALL ON accounts, transactions FROM app_writer;
代码语言:javascript
复制
// Rust 调用存储过程
async fn transfer(pool: &PgPool, from: i64, to: i64, amount: Decimal) -> Result<(), AppError> {
    sqlx::query("SELECT transfer_funds($1, $2, $3)")
        .bind(from)
        .bind(to)
        .bind(amount)
        .execute(pool)
        .await?;
    Ok(())
}

6.5 输入验证与类型约束

Rust 的类型系统是天然的第一道输入验证:

代码语言:javascript
复制
use serde::Deserialize;
use validator::Validate;

// ✅ 用类型和验证器约束输入
#[derive(Deserialize, Validate)]
pub struct SearchRequest {
    #[validate(length(min = 1, max = 100))]
    pub keyword: String,
    
    #[validate(range(min = 1, max = 100))]
    pub page: Option<u32>,
    
    #[validate(range(min = 1, max = 100))]
    pub per_page: Option<u32>,
    
    // 枚举类型天然限制取值范围
    pub sort: Option<SortField>,
    pub order: Option<SortOrder>,
}

#[derive(Deserialize)]
pub enum SortField {
    Name,
    Email,
    CreatedAt,
}

#[derive(Deserialize)]
pub enum SortOrder {
    Asc,
    Desc,
}

// Axum handler
async fn search(
    State(state): State<AppState>,
    Query(req): Query<SearchRequest>,
) -> Result<Json<Vec<User>>, AppError> {
    req.validate()?;  // 校验输入
    
    // 枚举自动保证 sort 字段只能是预定义值
    let sort_field = match req.sort.unwrap_or(SortField::CreatedAt) {
        SortField::Name => "name",
        SortField::Email => "email",
        SortField::CreatedAt => "created_at",
    };
    
    let order = match req.order.unwrap_or(SortOrder::Desc) {
        SortOrder::Asc => "ASC",
        SortOrder::Desc => "DESC",
    };
    
    let pattern = format!("%{}%", req.keyword);
    let page = req.page.unwrap_or(1) as i64;
    let per_page = req.per_page.unwrap_or(20) as i64;
    let offset = (page - 1) * per_page;
    
    let sql = format!(
        "SELECT id, name, email FROM users WHERE name LIKE $1 ORDER BY {} {} LIMIT $2 OFFSET $3",
        sort_field, order  // 来自枚举,安全
    );
    
    let users = sqlx::query_as::<_, User>(&sql)
        .bind(pattern)
        .bind(per_page)
        .bind(offset)
        .fetch_all(&state.db)
        .await?;
    
    Ok(Json(users))
}

多层验证:类型约束 → 验证器 → 白名单 → 参数化查询。四层过滤,注入几乎不可能穿透。

6.6 SQL 审计日志

在数据库层开启查询日志,用于事后追溯:

代码语言:javascript
复制
-- PostgreSQL pgAudit 扩展
CREATE EXTENSION pgaudit;

-- 配置审计级别
ALTER SYSTEM SET pgaudit.log = 'write, ddl';
ALTER SYSTEM SET pgaudit.log_parameter = on;

-- 或使用 log_statement(轻量级)
ALTER SYSTEM SET log_statement = 'ddl';
ALTER SYSTEM SET log_min_duration_statement = 1000;  -- 记录慢查询
代码语言:javascript
复制
// 应用层记录 SQL 执行(不含参数值,防止泄露)
tracing::debug!(
    table = "users",
    operation = "select",
    "executing parameterized query"
);

6.7 WAF 与输入过滤

在应用前面部署 WAF(Web Application Firewall),作为额外防线:

代码语言:javascript
复制
# Nginx + ModSecurity 示例规则
# 检测常见 SQL 注入特征
SecRule ARGS "@rx (?i)(union\s+select|insert\s+into|delete\s+from|drop\s+table|;\s*--)" \
    "id:1001,phase:2,deny,status:403,msg:'SQL Injection attempt'"

WAF 不是万能的(编码绕过、盲注都可能绕过),但能挡住大量自动化攻击脚本。


七、实战案例:一个安全搜索 API 的完整实现

把前面所有知识点串起来,实现一个生产级的安全搜索 API:

代码语言:javascript
复制
use axum::{
    extract::{Query, State},
    http::StatusCode,
    response::Json,
};
use serde::Deserialize;
use sqlx::QueryBuilder;
use validator::Validate;

// === 1. 输入模型 ===
#[derive(Deserialize, Validate)]
pub struct UserSearchRequest {
    #[validate(length(min = 1, max = 100))]
    pub keyword: Option<String>,
    
    pub status: Option<UserStatus>,
    pub sort: Option<SortField>,
    pub order: Option<SortOrder>,
    
    #[validate(range(min = 1, max = 10000))]
    pub page: Option<u32>,
    
    #[validate(range(min = 1, max = 100))]
    pub per_page: Option<u32>,
}

#[derive(Deserialize)]
pub enum UserStatus { Active, Inactive, Banned }

#[derive(Deserialize)]
pub enum SortField { Name, Email, CreatedAt, LastLogin }

#[derive(Deserialize)]
pub enum SortOrder { Asc, Desc }

// === 2. 输出模型(不含敏感字段) ===
#[derive(serde::Serialize, sqlx::FromRow)]
pub struct UserSummary {
    pub id: i64,
    pub name: String,
    pub avatar_url: Option<String>,
    pub created_at: chrono::DateTime<chrono::Utc>,
}

// === 3. Handler ===
pub async fn search_users(
    State(state): State<AppState>,
    Query(req): Query<UserSearchRequest>,
) -> Result<(StatusCode, Json<Vec<UserSummary>>), AppError> {
    // 输入验证
    req.validate()?;
    
    let page = req.page.unwrap_or(1) as i64;
    let per_page = req.per_page.unwrap_or(20) as i64;
    let offset = (page - 1) * per_page;
    
    // 动态构建查询
    let mut builder = QueryBuilder::new(
        "SELECT id, name, avatar_url, created_at FROM users WHERE 1=1"
    );
    
    // 关键词搜索(参数化)
    if let Some(keyword) = &req.keyword {
        let pattern = format!("%{}%", keyword);
        builder.push(" AND (name LIKE ").push_bind(pattern);
        builder.push(" OR email LIKE ").push_bind(format!("%{}%", keyword));
        builder.push(")");
    }
    
    // 状态过滤(枚举安全映射)
    if let Some(status) = &req.status {
        let status_str = match status {
            UserStatus::Active => "active",
            UserStatus::Inactive => "inactive",
            UserStatus::Banned => "banned",
        };
        builder.push(" AND status = ").push_bind(status_str);
    }
    
    // 排序(枚举安全映射)
    let sort_field = match req.sort.unwrap_or(SortField::CreatedAt) {
        SortField::Name => "name",
        SortField::Email => "email",
        SortField::CreatedAt => "created_at",
        SortField::LastLogin => "last_login_at",
    };
    let order = match req.order.unwrap_or(SortOrder::Desc) {
        SortOrder::Asc => "ASC",
        SortOrder::Desc => "DESC",
    };
    builder.push(format!(" ORDER BY {} {}", sort_field, order));
    
    // 分页(参数化)
    builder.push(" LIMIT ").push_bind(per_page);
    builder.push(" OFFSET ").push_bind(offset);
    
    // 执行
    let users = builder.build_query_as::<UserSummary>()
        .fetch_all(&state.db)
        .await
        .map_err(AppError::from)?;
    
    Ok((StatusCode::OK, Json(users)))
}

这段代码集合了:类型约束、输入验证、枚举白名单、QueryBuilder 参数化、输出脱敏、分页限制。是一个可以直接上生产的实现。


八、安全 Checklist

应用层

  • 所有 SQL 值参数化,零 format! 拼接用户输入
  • 列名/表名走白名单,不直接使用用户输入
  • IN 子句用 ANY($1)push_bind,不拼接字符串
  • 动态 SQL 用 QueryBuilder,值一律 push_bind
  • 使用 query!获取编译时检查(能用就用)
  • 输入用 validator 校验,长度、范围、格式
  • 枚举类型约束可选值,替代字符串白名单
  • 输出模型脱敏,不返回 password_hash、token 等敏感字段
  • Code Review 检查清单包含 SQL 注入项
  • CI 中跑 sqlx prepare 确保 SQL 编译时验证

数据库层

  • 最小权限:应用账号无 SUPERUSER、无 DDL 权限
  • 视图隔离:敏感列不直接暴露
  • RLS 策略:多租户数据隔离
  • 存储过程:复杂业务逻辑封装
  • 审计日志:pgAudit 或 log_statement
  • 连接加密:SSL/TLS 强制开启
  • 密码存储:bcrypt/argon2,不存明文
  • 定期备份:且备份文件加密存储

基础设施层

  • WAF 规则:拦截常见注入特征
  • 速率限制:防止盲注爆破
  • 参数化日志:日志不记录 SQL 参数值
  • 密钥管理:数据库密码用 Vault/Secret Manager
  • 网络隔离:数据库不暴露公网
  • 定期扫描:依赖漏洞 + SQL 注入扫描

九、常见问题

Q: Rust 的类型系统不能防止 SQL 注入吗?

A: 类型系统防止的是内存安全问题。SQL 注入是数据/代码边界混淆问题,需要参数化查询来解决。query! 宏的编译时检查能帮一部分忙,但前提是你用它,而不是用 format! 绕过它。

Q: 用了 ORM(Diesel/SeaORM)还需要担心 SQL 注入吗?

A: ORM 的 DSL 天然安全,但大多数 ORM 也提供了原生 SQL 接口(sql_queryStatement)。只要用了原生 SQL,就需要手动确保参数化。另外,动态排序、动态表名等场景在 ORM 中也可能退化为字符串拼接。

Q: 内部微服务间调用的参数需要参数化吗?

A: 需要。内部服务可能被横向移动的攻击者利用,也可能因为自己的 bug 传入恶意数据。零信任原则:不信任任何来源的输入。

Q: ORDER BY 不能参数化怎么办?

A: 用白名单。将合法的列名和排序方向定义为枚举或常量数组,用户输入与白名单比对后才使用。

Q: 如何在团队中推广 SQL 注入防范?

A: 三步走:(1) 在 Code Review 模板中加入 SQL 注入检查项;(2) 编写一个简单的 CI 脚本,扫描代码中的 format! + SQL 关键字组合;(3) 定期做安全培训,用真实案例说话。


十、总结

代码语言:javascript
复制
SQL 注入防御 = 参数化查询 + 白名单标识符 + 数据库最小权限 + 纵深防御

Rust 赋予我们强大的类型系统和编译时保障,但安全从来不是单一层面的工程:

  1. 1. 应用层:参数化查询是底线,QueryBuilder 是动态 SQL 的安全工具,枚举 + validator 是输入验证的标准组合
  2. 2. 数据库层:最小权限限制爆炸半径,RLS 实现租户隔离,视图脱敏敏感数据
  3. 3. 基础设施层:WAF 挡脚本小子,审计日志留追查线索,网络隔离减少攻击面

记住一个核心原则:永远不要把用户输入当代码执行。 无论是 SQL、Shell 命令还是模板渲染,这条原则都适用。

Rust 让我们在内存安全上高枕无忧,但在应用安全上,警钟必须长鸣。特别是当在 AI 辅助下编程 ,AI并不能给出令我们完全可信的代码,一定要做好安全检查。


本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-07-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Rust火箭工坊 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ❌ 你是否遇到过?
  • 一、先泼盆冷水:Rust 并不天然防 SQL 注入
  • 二、SQL 注入攻击面全景
    • 2.1 常见注入入口
    • 2.2 一个真实攻击链
  • 三、安全查询模式:三大主流方案
    • 3.1 SQLx:参数化查询 + 编译时检查
      • 第一层:参数化查询(运行时防护)
      • 第二层:编译时检查(query! 宏)
    • 3.2 Diesel:ORM 级别的类型安全
    • 3.3 SeaORM:动态查询的安全构建
    • 三大方案对比
  • 四、动态 SQL 的安全构建
    • 4.1 动态排序字段
    • 4.2 动态表名(多租户场景)
    • 4.3 动态 IN 子句
    • 4.4 动态 WHERE 条件组合
  • 五、六大危险反模式
    • 反模式 1:format! 拼接用户输入
    • 反模式 2:字符串插值
    • 反模式 3:信任"内部"参数
    • 反模式 4:手动转义引号
    • 反模式 5:正则过滤"危险字符"
    • 反模式 6:日志中泄露 SQL 参数
  • 六、数据库纵深防御
    • 6.1 最小权限原则
    • 6.2 使用视图限制数据访问
    • 6.3 PostgreSQL Row Level Security(RLS)
    • 6.4 存储过程封装
    • 6.5 输入验证与类型约束
    • 6.6 SQL 审计日志
    • 6.7 WAF 与输入过滤
  • 七、实战案例:一个安全搜索 API 的完整实现
  • 八、安全 Checklist
    • 应用层
    • 数据库层
    • 基础设施层
  • 九、常见问题
  • 十、总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档