Rust 安全开发手册:从安全代码到不可攻破的系统

121 阅读14分钟

本文首发公众号 猩猩程序员 欢迎关注

本文来自 yevh.github.io/rust-securi…

Rust 安全开发手册:从安全代码到不可攻破的系统

“Rust 可以防止你因内存错误而开枪打中自己的脚,但它阻止不了你把枪口对准用户的钱包。”
—— Rust 安全信徒守则


序章:Rust 安全的三大支柱

Rust 免费为你提供内存安全,但应用程序级的安全性必须通过纪律与规范来获得。本指南将教你如何像一位 Rustacean 一样思考,同时具备安全工程师的视角。因为在生产系统中,“大致安全”就像“稍微怀孕”一样,不是一个选项。

Rust 安全三要素:

  1. 类型安全(Type Safety) —— 让无效状态在类型系统中无法表达
  2. 错误安全(Error Safety) —— 将 panic 转化为可控的失败
  3. 机密安全(Secret Safety) —— 内存中的敏感信息应当如幽灵般存在,使用完就必须安全抹除

第 1 章:类型系统 —— 第一道防线

为什么基础类型是安全漏洞

每个 u64 在你的 API 中都可能成为一个价值百万美元的潜在漏洞:

// ❌ 灾难即将发生
fn transfer(from: u64, to: u64, amount: u64) -> Result<(), Error> {
    // 当一个凌晨 2 点疲惫的开发者把参数顺序写错会发生什么?
    // transfer(balance, user_id, amount) ← 💥 钱转错了
}

在传统语言中,这段代码可以正常编译、运行。在区块链背景下,它可能会从账户 67890 向用户 ID 12345 转账。代码本身没问题,只是把钱转错了地方。


Newtype 模式:零成本的类型安全

将所有有语义的基础类型包裹成独立的结构体:

// ✅ 无法误用
#[derive(Debug, Clone, Copy, PartialEq)]
struct UserId(u64);

#[derive(Debug, Clone, Copy, PartialEq)]
struct Balance(u64);

#[derive(Debug, Clone, Copy, PartialEq)]
struct TokenAmount(u64);

fn transfer(from: UserId, to: UserId, amount: TokenAmount) -> Result<(), Error> {
    // 现在从类型层面就无法把参数写反!
}

魔法点: 这些 wrapper 类型在编译时会被消除(零运行时开销),但能捕获 100% 的参数交换类错误。


真实案例:Merkle 根混淆事故

// ❌ 生产事故:所有 Merkle 根在类型上看起来一样
fn verify_proof(root: [u8; 32], proof: Vec<[u8; 32]>, leaf: [u8; 32]) -> bool {
    // merkle 校验逻辑
}

// 错误地把 balance 根传给了 nullifier 根的参数
let valid = verify_proof(balance_root, proof, nullifier_hash); // 💥 逻辑错误

// ✅ 防弹实现:为每种根定义独立类型
struct BalanceRoot([u8; 32]);
struct NullifierRoot([u8; 32]);

fn verify_balance_proof(root: BalanceRoot, proof: Vec<[u8; 32]>, leaf: [u8; 32]) -> bool {
    // 校验逻辑
}

// 现在无法编译——类型系统救了你
let valid = verify_balance_proof(nullifier_root, proof, leaf); // ❌ 编译错误

哪些时候该使用 Newtype?答案是:几乎总是

  • ID、哈希、Merkle 根、密钥、地址、nonce
  • 业务值(如价格、余额、数量)
  • 已验证的数据(如 Email、手机号)
  • 有特定含义的数组索引

第 2 章:错误处理 —— 当 unwrap() 变成武器

unwrap():定时炸弹

在 Web3 和金融系统中,panic 不只是崩溃,它意味着拒绝服务攻击(DoS)

// ❌ 存在 DoS 漏洞
fn calculate_fee(amount: u64, rate: u64) -> u64 {
    amount.checked_mul(rate).unwrap() / 10000  // 💥 panic = 烧光 gas 且交易失败
}

攻击者可以提交特定输入使乘法溢出,导致 panic,用户的 gas 被消耗但交易无效。重复发起这种攻击即可瘫痪智能合约。


使用 ? 运算符:优雅降级

// ✅ 优雅失败
fn calculate_fee_safe(amount: u64, rate: u64) -> Result<u64, FeeError> {
    let fee_total = amount
        .checked_mul(rate)
        .ok_or(FeeError::Overflow)?;  // 返回错误而非 panic

    Ok(fee_total / 10000)
}

? 运算符的魔力:

  • Ok(value) → 提取值,继续执行
  • Err(error) → 立即返回错误
  • 无 panic、无 DoS,只有可控失败

何时使用 unwrap() 是安全的?

有时 unwrap() 可以被数学证明是安全的

// ✅ 安全:vector 刚被创建,已知非空
let numbers = vec![1, 2, 3, 4, 5];
let first = numbers.get(0).expect("vector has 5 elements");

// ✅ 安全:先验证过条件
if !user_input.is_empty() {
    let first_char = user_input.chars().next().unwrap();
}

规则: 如果你无法写出注释解释为何 unwrap() 一定不会失败,那它很可能会失败。


第 3 章:整数运算 —— 钱去哪儿了?

静默杀手:整数溢出

Rust 在 release 模式下默认悄悄溢出,这会让你的财务计算变成伪随机生成器

// ❌ 金额被悄悄破坏
fn add_to_balance(current: u64, deposit: u64) -> u64 {
    current + deposit  // 如果溢出:u64::MAX + 1 = 0
}

// 用户拥有最大余额,充值 1 wei → 余额变成 0!

这并非理论问题,整数溢出已经在生产中造成了真实的金钱损失。


安全整数运算三大支柱

1. Checked Arithmetic(带检查的运算) :关键金额操作必用
// ✅ 检测溢出
fn add_to_balance_safe(current: u64, deposit: u64) -> Result<u64, BalanceError> {
    current
        .checked_add(deposit)
        .ok_or(BalanceError::Overflow)
}

适用场景:金额、价格、余额、关键逻辑


2. Saturating Arithmetic(饱和运算) :用于计数器和限制
// ✅ 强制边界
fn apply_penalty(reputation: u32, penalty: u32) -> u32 {
    reputation.saturating_sub(penalty)  // 最低为 0
}

fn increment_counter(count: u32) -> u32 {
    count.saturating_add(1)  // 超过最大值就固定在 MAX
}

适用场景:计数器、声誉系统、限速器


3. Wrapping Arithmetic(包裹运算) :用于哈希函数
// ✅ 明确的环绕行为
fn hash_combine(hash: u32, value: u32) -> u32 {
    hash.wrapping_mul(31).wrapping_add(value)  // 预期的溢出
}

适用场景:密码学操作中数学正确的溢出


财务运算最佳实践

// ❌ 错误:浮点精度误差 + 舍入问题
fn calculate_fee_wrong(amount: u64, rate_percent: f64) -> u64 {
    (amount as f64 * rate_percent / 100.0).round() as u64
}

// ✅ 正确:使用整数运算 + 明确舍入方式
fn calculate_fee_correct(amount: u64, rate_bps: u64) -> Result<u64, Error> {
    let fee_precise = amount
        .checked_mul(rate_bps)
        .ok_or(Error::Overflow)?;
    
    let fee = fee_precise / 10000;
    if fee_precise % 10000 > 0 {
        fee.checked_add(1).ok_or(Error::Overflow)
    } else {
        Ok(fee)
    }
}

fn calculate_payout(amount: u64, rate_bps: u64) -> Result<u64, Error> {
    amount
        .checked_mul(rate_bps)
        .and_then(|x| x.checked_div(10000))
        .ok_or(Error::Overflow)
}

黄金法则:

  • 费用/手续费 → 向上舍入(不漏收)
  • 退款/付款 → 向下舍入(不多付)
  • 先乘后除(顺序很关键)
  • 用基点(bps)代替浮点百分比
  • release 模式下启用溢出检查:
[profile.release]
overflow-checks = true

第 4 章:密码学与机密信息 —— 数字锁的艺术

随机数:安全的基石

密码学安全往往归结为:“攻击者能否预测这个数字?”

// ❌ 可预测 = 不安全
use rand::{Rng, rngs::StdRng, SeedableRng};
let mut rng = StdRng::seed_from_u64(42);  // 相同种子 = 相同随机序列!
let private_key: [u8; 32] = rng.gen();    // 可预测 = 资金被盗

// ✅ 密码学安全的随机数生成
use rand::rngs::OsRng;
let private_key: [u8; 32] = OsRng.gen();  // 从操作系统熵池获取

规则: 如果是保护秘密或资金,务必使用 OsRng。游戏或测试时,使用可确定的伪随机数生成器没问题。


机密生命周期管理

秘密信息不会像你想的那样一消失就没了:

// ❌ 密码永远留在内存中
let mut password = String::from("super_secret_password");
password.clear();  // 只是清空了长度,数据仍在内存中!

// ✅ 安全擦除秘密
use zeroize::{Zeroize, Zeroizing};

// 手动擦除
let mut secret = [0u8; 32];
OsRng.fill_bytes(&mut secret);
// 使用 secret ...
secret.zeroize();  // 用零覆盖内存

// 自动擦除
let api_key = Zeroizing::new(load_api_key());
// 变量被销毁时自动擦除

Debug Trait 陷阱

// ❌ 机密泄露在日志中
#[derive(Debug)]
struct ApiCredentials {
    key: String,
    secret: String,
}

let creds = ApiCredentials { /* ... */ };
println!("Credentials: {:?}", creds);  // 日志记录了秘密信息!

// ✅ 日志脱敏处理
use secrecy::{Secret, ExposeSecret};

struct ApiCredentials {
    key: String,
    secret: Secret<String>,
}

let creds = ApiCredentials {
    key: "public_key".to_string(),
    secret: Secret::new("very_secret".to_string()),
};

println!("Credentials: key={}, secret=[REDACTED]", creds.key);

// 仅在确实需要时访问秘密
let actual_secret = creds.secret.expose_secret();

安全密码学模式

// ✅ 认证加密(永远不要用裸加密)
use aes_gcm::{Aes256Gcm, Key, Nonce, aead::{Aead, NewAead}};

fn encrypt_secure(plaintext: &[u8], key: &[u8; 32]) -> Result<Vec<u8>, Error> {
    let cipher = Aes256Gcm::new(Key::from_slice(key));
    let nonce: [u8; 12] = OsRng.gen();

    let ciphertext = cipher
        .encrypt(Nonce::from_slice(&nonce), plaintext)
        .map_err(|_| Error::EncryptionFailed)?;

    // 将 nonce 预置于密文开头,方便存储和解密
    let mut result = nonce.to_vec();
    result.extend_from_slice(&ciphertext);
    Ok(result)
}

// ✅ 常数时间比较(防止时序攻击)
use subtle::ConstantTimeEq;

fn verify_mac(expected: &[u8], actual: &[u8]) -> bool {
    expected.ct_eq(actual).into()  // 总是耗时相同
}

第 5 章:注入攻击 —— 字符串变成代码的噩梦

SQL 注入:永恒的敌人

即使在 Rust 中,字符串格式化也可能导致注入漏洞:

// ❌ SQL 注入
fn find_user(name: &str) -> Result<User, Error> {
    let query = format!("SELECT * FROM users WHERE name = '{}'", name);
    // 如果 name = "'; DROP TABLE users; --",数据库就完了
    database.execute(&query)
}

// ✅ 参数化查询
use sqlx::PgPool;

async fn find_user_safe(pool: &PgPool, name: &str) -> Result<User, Error> {
    let user = sqlx::query_as!(
        User,
        "SELECT * FROM users WHERE name = $1",  // $1 自动转义
        name
    )
    .fetch_one(pool)
    .await?;

    Ok(user)
}

命令注入:Shell 的恶作剧

// ❌ 命令注入
fn search_logs(pattern: &str) -> Result<String, Error> {
    let output = Command::new("sh")
        .arg("-c")
        .arg(format!("grep {} /var/log/app.log", pattern))  // pattern = "; rm -rf /"
        .output()?;

    Ok(String::from_utf8(output.stdout)?)
}

// ✅ 安全的参数传递,避免 shell 解析
fn search_logs_safe(pattern: &str) -> Result<String, Error> {
    let output = Command::new("grep")
        .arg(pattern)  // 作为字面量参数
        .arg("/var/log/app.log")
        .output()?;

    Ok(String::from_utf8(output.stdout)?)
}

第 6 章:异步 Rust —— 无痛并发

阻塞操作陷阱

异步 Rust 很快,直到你无意中阻塞了整个运行时:

// ❌ 阻塞了整个异步运行时
async fn hash_password_wrong(password: &str) -> String {
    // CPU 密集型操作,阻塞所有异步任务!
    expensive_password_hash(password)
}

// ✅ 放到线程池
async fn hash_password_right(password: String) -> Result<String, Error> {
    let hash = tokio::task::spawn_blocking(move || {
        expensive_password_hash(&password)
    })
    .await
    .map_err(|_| Error::TaskFailed)?;

    Ok(hash)
}

何时使用 spawn_blocking

  • CPU 密集型工作(哈希、解析、压缩)
  • 同步 IO(文件操作、阻塞数据库调用)
  • 任意耗时数毫秒以上的操作

“锁跨 await” 死锁

// ❌ 死锁陷阱
async fn dangerous_pattern(shared: &Mutex<Vec<String>>) {
    let mut data = shared.lock().unwrap();  // 获取锁
    data.push("item".to_string());

    some_async_operation().await;  // ⚠️ 持锁跨 await

    data.push("another".to_string());
}  // 直到这里锁才释放,其他任务阻塞!

// ✅ 安全:await 前释放锁
async fn safe_pattern(shared: &tokio::sync::Mutex<Vec<String>>) {
    {
        let mut data = shared.lock().await;
        data.push("item".to_string());
    }  // 锁在这里释放

    some_async_operation().await;  // 无锁状态

    {
        let mut data = shared.lock().await;
        data.push("another".to_string());
    }  // 再次释放锁
}

取消安全:隐藏的异步风险

每个 .await 都是潜在取消点,可能导致未来(future)被丢弃:

// ❌ 取消不安全
async fn transfer_funds_unsafe(from: &Account, to: &Account, amount: u64) {
    from.balance -= amount;  // ⚠️ 这里可能被取消
    network_commit().await;  // 取消点!
    to.balance += amount;    // 可能永远不执行 → 钱丢了!
}

// ✅ 取消安全:原子状态更新
async fn transfer_funds_safe(from: &Account, to: &Account, amount: u64) -> Result<(), Error> {
    // 先完成所有异步工作
    let transfer_id = prepare_transfer(amount).await?;

    // 然后用阻塞任务执行原子更新(无取消点)
    tokio::task::spawn_blocking(move || {
        from.balance -= amount;
        to.balance += amount;
        commit_transfer(transfer_id);
    }).await?;

    Ok(())
}

第 7 章:Web3 与智能合约安全

缺失签名者检查(以 Solana 为例)

// ❌ 授权绕过漏洞
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
    let user_account = &mut ctx.accounts.user_account;
    // 漏洞:从未验证 user_account 是否签署了交易!

    user_account.balance -= amount;
    Ok(())
}

// ✅ 验证授权
pub fn withdraw_safe(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
    let user_account = &mut ctx.accounts.user_account;

    // 关键:验证账户持有者是否授权
    require!(user_account.is_signer, ErrorCode::MissingSigner);

    // 额外安全:检查余额是否足够
    require!(
        user_account.balance >= amount,
        ErrorCode::InsufficientFunds
    );

    user_account.balance -= amount;
    Ok(())
}

由程序派生地址(PDA)验证

// ❌ 盲目信任用户输入
pub fn update_vault(ctx: Context<UpdateVault>) -> Result<()> {
    let vault = &mut ctx.accounts.vault;
    // 漏洞:攻击者可传入自己控制的钱库地址!
    vault.amount += 100;
    Ok(())
}

// ✅ 验证 PDA 所有权
pub fn update_vault_safe(ctx: Context<UpdateVault>) -> Result<()> {
    let vault = &mut ctx.accounts.vault;

    // 重新计算预期 PDA
    let (expected_vault, _bump) = Pubkey::find_program_address(
        &[b"vault", ctx.accounts.user.key().as_ref()],
        ctx.program_id,
    );

    require!(vault.key() == expected_vault, ErrorCode::InvalidVault);

    vault.amount += 100;
    Ok(())
}

智能合约的确定性

// ❌ 非确定性(导致共识失败)
pub fn create_auction(duration_hours: u64) -> Result<()> {
    let start_time = SystemTime::now()  // 各验证节点时间不同!
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs();
    Ok(())
}

// ✅ 确定性
pub fn create_auction_safe(ctx: Context<CreateAuction>, duration_hours: u64) -> Result<()> {
    let clock = Clock::get()?;  // 区块链提供的统一时间戳
    let start_time = clock.unix_timestamp as u64;  // 所有验证节点相同
    Ok(())
}

第 8 章:unsafe 关键词 —— 小心使用

书写安全契约

每个 unsafe 块必须详细说明其安全前提:

// ❌ 危险示例:无安全文档
unsafe fn write_bytes(ptr: *mut u8, bytes: &[u8]) {
    std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr, bytes.len());
}

// ✅ 安全示例:明确安全契约
/// 复制字节到指定指针。
///
/// # Safety
/// 
/// - `ptr` 必须非空且对齐正确
/// - `ptr` 指向的内存必须可写,至少包含 `bytes.len()` 字节
/// - 内存区域与 `bytes` 不重叠
/// - 本调用期间无其他线程访问该内存区域
unsafe fn write_bytes_safe(ptr: *mut u8, bytes: &[u8]) {
    debug_assert!(!ptr.is_null(), "ptr 不能为 null");
    debug_assert!(ptr.is_aligned(), "ptr 必须对齐");

    // SAFETY: 调用者保证以上条件
    std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr, bytes.len());
}

FFI 安全

extern "C" {
    fn c_hash_function(input: *const u8, len: usize, output: *mut u8);
}

// ✅ 安全的 FFI 包装器
/// 使用 C 库计算哈希
pub fn hash_bytes(data: &[u8]) -> [u8; 32] {
    let mut output = [0u8; 32];

    // SAFETY:
    // - data.as_ptr() 有效,长度为 data.len()
    // - output.as_mut_ptr() 有效,长度为 32
    // - C 函数保证写入恰好 32 字节
    unsafe {
        c_hash_function(data.as_ptr(), data.len(), output.as_mut_ptr());
    }

    output
}

第 9 章:开发与部署安全

依赖安全

# ✅ 安全审计流水线
cargo audit                    # 检查依赖漏洞
cargo build --release --locked # 使用锁定版本构建
cargo clippy -- -D warnings   # 开启安全相关 lint 规则

安全导向的编译器设置

[profile.release]
overflow-checks = true     # 捕获整数溢出
debug-assertions = true    # release 也保留 debug_assert!
strip = true               # 去除调试符号
lto = true                 # 链接时优化
codegen-units = 1          # 更好的优化效果

# 安全相关的 clippy 配置
[alias]
secure-check = [
    "clippy", "--all-targets", "--all-features", "--",
    "-D", "clippy::unwrap_used",
    "-D", "clippy::expect_used",
    "-D", "clippy::indexing_slicing",
    "-D", "clippy::panic",
]

基于属性的测试

use proptest::prelude::*;

// 测试安全属性,而非仅限理想情况
proptest! {
    #[test]
    fn transfer_never_creates_money(
        initial_from in 0u64..=1_000_000,
        initial_to in 0u64..=1_000_000,
        amount in 0u64..=1_000_000
    ) {
        let mut from = Account { balance: initial_from };
        let mut to = Account { balance: initial_to };
        let total_before = initial_from + initial_to;

        let _ = transfer(&mut from, &mut to, amount);

        let total_after = from.balance + to.balance;
        prop_assert_eq!(total_before, total_after, "钱被创造或毁灭了!");
    }
}

第 10 章:安全心态

威胁建模问题

对于每个函数,问自己:

  • 输入有可能是恶意的吗?
  • 这个函数每秒被调用百万次会怎样?
  • 多线程同时调用会导致什么问题?
  • 攻击者最坏能做什么?
  • 生产环境下哪些假设可能不成立?

深度防御

// ✅ 多层防御
pub fn process_payment(
    user: &User,
    amount: TokenAmount,
    signature: &Signature,
) -> Result<(), PaymentError> {
    // 第一层:身份验证
    verify_signature(&user.public_key, signature)?;

    // 第二层:授权检查
    user.check_payment_permissions()?;

    // 第三层:输入验证
    if amount.0 == 0 {
        return Err(PaymentError::ZeroAmount);
    }

    // 第四层:业务规则检查
    if amount.0 > user.balance.0 {
        return Err(PaymentError::InsufficientFunds);
    }

    // 第五层:速率限制
    user.check_rate_limit()?;

    // 第六层:溢出保护
    let new_balance = user.balance.0
        .checked_sub(amount.0)
        .ok_or(PaymentError::ArithmeticError)?;

    // 最终执行
    user.balance = Balance(new_balance);
    user.record_payment(amount);

    Ok(())
}

终极安全检查表

部署 Rust 代码到生产环境之前:

  • 类型安全

    • 所有基本类型封装成语义新类型
    • API 参数无法错位
    • 业务概念编码到类型中
  • 错误处理

    • 生产路径无 unwrap()panic!()
    • 所有 Result 正确使用 ?
    • 明确错误类型区分失败模式
  • 算术安全

    • 资金/余额操作使用带检查函数
    • release 模式启用溢出检查
    • 费用和退款舍入正确
  • 密码学

    • 所有安全随机数用 OsRng
    • 秘密数据用 Zeroizingsecrecy 包裹
    • 日志无机密输出
    • MAC/哈希使用常数时间比较
  • 注入防御

    • 所有 SQL 使用参数化查询
    • 无格式化命令执行,避免 shell 注入
    • 输入校验和清理
  • 异步安全

    • 无阻塞异步代码
    • 不持锁跨 .await
    • 状态更新取消安全
  • 智能合约安全

    • 验证所有签名者
    • PDA 地址验证
    • 保证确定性行为
  • unsafe 代码

    • 文档化安全契约
    • debug 下断言
    • 验证 FFI 边界
  • 开发安全

    • 通过 cargo audit
    • 构建加 --locked
    • 开启安全相关 lint
    • 基于属性的测试覆盖
  • 部署安全

    • 开启溢出检查
    • 去除调试符号
    • 依赖锁定与审计

最后的箴言:Rust 安全三大定律

  • 让无效状态无法表示 —— 用类型系统防止编译期错误
  • 明确且优雅地失败 —— 将 panic 转为受控的 Result
  • 信任但需验证 —— 尤其是安全与非安全代码边界处

给笔记本贴纸的简短格言

“内存安全免费,应用安全有价——但那价钱就是好习惯。”


记住:Rust 给你安全的先发优势,但它不是万能钥匙。最安全的代码是不写的代码,其次是写代码时头脑清楚、懂得攻击思路的开发者写的代码。

现在,去构建不仅快速且内存安全,更是坚不可摧的安全系统吧。用户的钱财指望着你。🦀🔒💰

本文首发公众号 猩猩程序员 欢迎关注