Skip to content

Latest commit

 

History

History
514 lines (399 loc) · 15.9 KB

File metadata and controls

514 lines (399 loc) · 15.9 KB

ERC Framework 安全审计报告

审计信息

  • 审计日期: 2026-05-11
  • 审计范围: 全部源代码、配置文件、部署脚本
  • 审计工具: 静态代码分析、手动审查

1. 发现摘要

编号 严重程度 标题 位置 状态
F-01 🔴 Critical 上游URL未做白名单限制,存在SSRF风险 erc-gateway/src/proxy.rs ✅ 已修复
F-02 🔴 Critical 限流并发竞态条件,可能导致计数错误 erc-gateway/src/main.rs ✅ 已修复
F-03 🟠 High 多处使用unwrap()/expect(),可被外部输入触发panic 多个文件 ✅ 已修复
F-04 🟠 High 未限制请求体大小,可能导致内存耗尽 erc-gateway/src/main.rs ✅ 已修复
F-05 🟡 Medium 上游响应未验证JSON结构 erc-gateway/src/proxy.rs 未修复
F-06 🟡 Medium 数据库文件权限未自动设置为600 erc-gateway/src/main.rs 未修复
F-07 🟡 Medium 构建脚本未使用--locked锁定依赖版本 build.ps1 未修复
F-08 🟡 Medium Nonce未实现原子递增机制 erc-gateway/src/recorder.rs 未修复
F-09 🟢 Low bincode序列化未明确配置fixed int encoding erc-core/src/receipt.rs 未修复
F-10 ℹ️ Info 缺少安全最佳实践部署文档 docs/ 未修复

2. 详细发现

[F-01] 🔴 Critical 上游URL未做白名单限制,存在SSRF风险

位置: erc-gateway/src/proxy.rs:5-63, erc-gateway/src/types.rs:15-16

描述: 代理模块直接使用环境变量UPSTREAM_URL构造请求,未验证目标域名是否在白名单内。攻击者可通过修改环境变量或利用配置错误,让网关请求内网地址(如http://169.254.169.254/latest/meta-data/)或内部服务。

代码片段:

// erc-gateway/src/types.rs:15-16
upstream_url: std::env::var("UPSTREAM_URL")
    .unwrap_or("https://api.deepseek.com/anthropic/v1/messages".into()),
// erc-gateway/src/proxy.rs:11-12
let response = client
    .post(upstream_url)

攻击场景:

  1. 攻击者设置UPSTREAM_URL=http://169.254.169.254/latest/meta-data/
  2. 网关向AWS元数据服务发起请求
  3. 获取IAM凭证等敏感信息

修复建议:

  1. 新增环境变量ALLOWED_UPSTREAM_HOSTS(逗号分隔白名单域名)
  2. forward_to_upstream函数中验证URL host是否在白名单内
  3. 拒绝IP地址格式的URL,仅允许域名
// 建议修复代码
fn validate_upstream_url(url: &str, allowed_hosts: &[String]) -> Result<(), String> {
    let parsed = url::Url::parse(url).map_err(|e| format!("Invalid URL: {}", e))?;
    let host = parsed.host_str().ok_or("No host in URL")?;
    
    // 拒绝IP地址
    if host.parse::<std::net::IpAddr>().is_ok() {
        return Err("IP addresses not allowed".into());
    }
    
    if !allowed_hosts.iter().any(|allowed| host.ends_with(allowed)) {
        return Err(format!("Host {} not in whitelist", host));
    }
    Ok(())
}

参考: OWASP SSRF Prevention Cheat Sheet


[F-02] 🔴 Critical 限流并发竞态条件,可能导致计数错误

位置: erc-gateway/src/main.rs:86-127

描述: 限流逻辑使用SQLite进行计数,但在高并发场景下存在TOCTOU(Time-of-check to time-of-use)竞态条件。多个请求可能同时读取相同的remaining值,导致实际请求数超过限制。

代码片段:

// erc-gateway/src/main.rs:88-115
let row = db.query_row(
    "SELECT remaining, window_end FROM rate_limits WHERE ip = ?1",
    [&ip],
    |row| Ok((row.get::<_, u32>(0)?, row.get::<_, i64>(1)?)),
);

let remaining: u32;

match row {
    Ok((rem, end)) => {
        if now > end {
            db.execute(
                "UPDATE rate_limits SET remaining = ?1, window_end = ?2 WHERE ip = ?3",
                rusqlite::params![max_calls_per_hour - 1, now + window_seconds, ip],
            ).ok();
            remaining = max_calls_per_hour;
        } else {
            remaining = rem;  // 竞态窗口:多个请求可能同时读取相同的rem
        }
    }
    // ...
}

攻击场景:

  1. 攻击者使用100个并发请求
  2. 所有请求同时读取remaining=1
  3. 所有请求都通过检查,实际发送100+请求

修复建议: 使用SQLite的原子操作或事务:

// 建议修复代码 - 使用原子UPDATE RETURNING
let remaining: u32 = db.query_row(
    "UPDATE rate_limits 
     SET remaining = CASE 
         WHEN window_end < ?1 THEN ?2 - 1
         WHEN remaining > 0 THEN remaining - 1
         ELSE 0
     END,
     window_end = CASE
         WHEN window_end < ?1 THEN ?1 + ?3
         ELSE window_end
     END
     WHERE ip = ?4
     RETURNING remaining",
    rusqlite::params![now, max_calls_per_hour, window_seconds, ip],
    |row| row.get(0),
).map_err(|_| StatusCode::TOO_MANY_REQUESTS)?;

if remaining == 0 {
    return Err(StatusCode::TOO_MANY_REQUESTS);
}

参考: SQLite UPSERT


[F-03] 🟠 High 多处使用unwrap()/expect(),可被外部输入触发panic

位置: 多个文件(共27处)

描述: 代码中大量使用unwrap()expect(),当遇到无效输入或外部数据异常时会导致进程panic,造成拒绝服务。

关键风险点:

  1. erc-gateway/src/main.rs:67-68 - 网络绑定panic
let listener = tokio::net::TcpListener::bind(&config.listen_addr)
    .await
    .unwrap();  // 地址已被占用时panic
axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await.unwrap();
  1. erc-core/src/receipt.rs:141-145 - 序列化panic
pub fn to_json(&self) -> String {
    serde_json::to_string(self).unwrap()  // 理论上不会失败,但不安全
}

pub fn to_canonical_bytes(&self) -> Vec<u8> {
    bincode::serialize(self).expect("Bincode serialization failed")
}
  1. erc-store/src/lib.rs:17-23 - 数据库锁panic
let conn = self.conn.lock().unwrap();  // 锁被污染时panic

修复建议:

  1. 将所有unwrap()替换为?操作符或显式错误处理
  2. 对于网络绑定,返回有意义的错误信息
  3. 对于数据库操作,使用lock().map_err()处理锁错误
// 建议修复代码
pub fn to_json(&self) -> Result<String, String> {
    serde_json::to_string(self).map_err(|e| e.to_string())
}

let conn = self.conn.lock().map_err(|_| "Database lock poisoned".to_string())?;

参考: Rust Error Handling Book


[F-04] 🟠 High 未限制请求体大小,可能导致内存耗尽

位置: erc-gateway/src/main.rs:79

描述: 代理处理器直接解析JSON请求体,未限制大小。攻击者可发送超大请求(如100MB),导致内存耗尽。

代码片段:

// erc-gateway/src/main.rs:79
Json(body): Json<Value>,  // 无大小限制

攻击场景:

# 攻击者发送100MB请求
curl -X POST http://target:8080/v1/messages \
  -H "Content-Type: application/json" \
  -d '{"messages":[{"role":"user","content":"'$(python3 -c "print('A'*100000000)")'"}]}'

修复建议: 使用axum的DefaultBodyLimit中间件:

use axum::extract::DefaultBodyLimit;

let app = Router::new()
    .route("/v1/messages", post(proxy_handler))
    .layer(DefaultBodyLimit::max(10 * 1024 * 1024))  // 10MB限制
    .with_state(state);

参考: axum Body Limit


[F-05] 🟡 Medium 上游响应未验证JSON结构

位置: erc-gateway/src/proxy.rs:49-60

描述: 代理仅验证响应是否为有效JSON,未检查是否包含预期字段。恶意上游可返回结构异常的JSON,导致后续处理错误。

代码片段:

// erc-gateway/src/proxy.rs:49-60
let response_body: Value = serde_json::from_str(&response_text)
    .map_err(|e| {
        // 仅检查JSON有效性,未检查结构
        format!("解析上游响应失败: {}。原始响应预览: {}", e, preview)
    })?;

修复建议:

  1. 定义上游响应的JSON Schema
  2. 使用jsonschema crate验证响应结构
  3. 拒绝包含意外字段的响应
// 建议修复代码
use jsonschema::JSONSchema;

const UPSTREAM_RESPONSE_SCHEMA: &str = r#"{
    "type": "object",
    "required": ["id", "content"],
    "properties": {
        "id": {"type": "string"},
        "content": {"type": "array"}
    },
    "additionalProperties": false
}"#;

fn validate_response(body: &Value) -> Result<(), String> {
    let schema = JSONSchema::compile(&serde_json::from_str(UPSTREAM_RESPONSE_SCHEMA).unwrap())
        .map_err(|e| format!("Schema compilation failed: {}", e))?;
    
    schema.validate(body).map_err(|errors| {
        let errors: Vec<String> = errors.map(|e| e.to_string()).collect();
        format!("Response validation failed: {}", errors.join(", "))
    })
}

参考: JSON Schema Validation


[F-06] 🟡 Medium 数据库文件权限未自动设置为600

位置: erc-gateway/src/main.rs:36-37, erc-gateway/src/recorder.rs:19-20

描述: SQLite数据库文件创建时使用默认权限(通常是644),其他用户可读取审计数据。

代码片段:

// erc-gateway/src/main.rs:36-37
let rate_db = rusqlite::Connection::open("erc_rate_limits.db")
    .expect("无法打开限流数据库");

修复建议:

  1. 在Unix系统上使用std::os::unix::fs::PermissionsExt设置权限
  2. 在Windows上使用ACL限制访问
  3. 创建后立即设置权限
// 建议修复代码
#[cfg(unix)]
fn set_db_permissions(path: &str) -> std::io::Result<()> {
    use std::os::unix::fs::PermissionsExt;
    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
}

let rate_db = rusqlite::Connection::open("erc_rate_limits.db")
    .expect("无法打开限流数据库");
set_db_permissions("erc_rate_limits.db").ok();

参考: SQLite Security


[F-07] 🟡 Medium 构建脚本未使用--locked锁定依赖版本

位置: build.ps1:4-5

描述: 构建脚本使用cargo build而非cargo build --locked,可能导致不同时间构建使用不同依赖版本,引入不可预测的行为。

代码片段:

# build.ps1:4-5
cargo build --release -p erc-gateway
cargo build --release -p erc-query-api

修复建议:

# 建议修复代码
cargo build --release --locked -p erc-gateway
cargo build --release --locked -p erc-query-api

参考: Cargo Book - Locked


[F-08] 🟡 Medium Nonce未实现原子递增机制

位置: erc-gateway/src/recorder.rs:157

描述: Receipt的nonce硬编码为0,未实现防重放的原子递增机制。

⚠️ 状态更新: 用户反馈已在 erc-store/src/lib.rs 中实现 UPSERT RETURNING,但当前仓库代码中未找到该实现。需确认是否在其他分支或本地环境。

代码片段:

// erc-gateway/src/recorder.rs:157
let receipt = ExecutionReceipt::new(
    &execution_id,
    &trace_id,
    actor,
    agent,
    model,
    action,
    0,  // 硬编码nonce=0
    custody,
);

修复建议:

  1. 使用SQLite的UPSERT RETURNING实现原子递增
  2. 基于agent_id维护独立的nonce序列
// 建议修复代码
let nonce: u64 = db.query_row(
    "INSERT INTO nonces (agent_id, nonce) 
     VALUES (?1, 1)
     ON CONFLICT(agent_id) DO UPDATE SET nonce = nonce + 1
     RETURNING nonce",
    [&agent_id],
    |row| row.get(0),
)?;

参考: SQLite UPSERT


[F-09] 🟢 Low bincode序列化未明确配置fixed int encoding

位置: erc-core/src/receipt.rs:144-146

描述: bincode默认使用variable int encoding,不同版本可能产生不同字节序列,影响签名验证的跨版本兼容性。

代码片段:

// erc-core/src/receipt.rs:144-146
pub fn to_canonical_bytes(&self) -> Vec<u8> {
    bincode::serialize(self).expect("Bincode serialization failed")
}

修复建议:

// 建议修复代码
use bincode::Options;

pub fn to_canonical_bytes(&self) -> Vec<u8> {
    bincode::DefaultOptions::new()
        .with_fixint_encoding()
        .with_little_endian()
        .serialize(self)
        .expect("Bincode serialization failed")
}

参考: bincode Configuration


[F-10] ℹ️ Info 缺少安全最佳实践部署文档

位置: docs/

描述: 文档目录缺少安全配置指南,包括:

  • Token生成和管理
  • 网络隔离建议
  • 文件权限设置
  • 监控和告警配置

修复建议: 创建docs/SECURITY.md文档,包含:

  1. 生产环境部署检查清单
  2. Token轮换策略
  3. 网络拓扑建议
  4. 安全监控指标

3. 附录

3.1 依赖漏洞详情

需要运行cargo audit检查以下依赖:

依赖 版本 用途 潜在风险
axum 0.7 Web框架 需检查CVE
tokio 1.x 异步运行时 需检查CVE
reqwest 0.12 HTTP客户端 需检查CVE
rusqlite 0.31 SQLite绑定 需检查CVE
ed25519-dalek 2.1 签名算法 需检查CVE
serde_json 1.0 JSON解析 需检查CVE

建议操作:

cargo install cargo-audit
cargo audit

3.2 Unsafe代码清单

无unsafe代码 - 项目未使用任何unsafe块,符合Rust安全最佳实践。

3.3 Panic热点清单

文件 行号 代码 风险等级
erc-gateway/src/main.rs 67 .unwrap() (TcpListener::bind) 🟠 High
erc-gateway/src/main.rs 68 .unwrap() (axum::serve) 🟠 High
erc-gateway/src/main.rs 37 .expect() (打开限流数据库) 🟡 Medium
erc-gateway/src/main.rs 44 .expect() (创建限流表) 🟡 Medium
erc-gateway/src/recorder.rs 20 .expect() (打开SQLite数据库) 🟡 Medium
erc-gateway/src/recorder.rs 38 .expect() (创建数据库表) 🟡 Medium
erc-core/src/receipt.rs 141 .unwrap() (JSON序列化) 🟢 Low
erc-core/src/receipt.rs 145 .expect() (bincode序列化) 🟢 Low
erc-store/src/lib.rs 17 .unwrap() (数据库锁) 🟡 Medium
erc-store/src/lib.rs 30 .unwrap() (prepare语句) 🟡 Medium
erc-store/src/lib.rs 31 .unwrap() (query_map) 🟡 Medium
erc-query-api/src/main.rs 55 .unwrap() (CORS解析) 🟡 Medium
erc-query-api/src/main.rs 69 .unwrap() (TcpListener::bind) 🟠 High
erc-query-api/src/main.rs 70 .unwrap() (axum::serve) 🟠 High

3.4 安全配置检查清单

  • 设置ERC_API_TOKEN环境变量(强制)
  • 设置ALLOWED_UPSTREAM_HOSTS白名单(待实现)
  • 设置ERC_CORS_ORIGIN限制跨域访问
  • 数据库文件权限设置为600
  • 使用非特权用户运行进程
  • 启用HTTPS(生产环境)
  • 配置日志不包含敏感信息
  • 定期运行cargo audit

4. 总结

ERC Framework在安全设计上有良好基础(无unsafe代码、使用ed25519签名、强制API Token),但存在以下关键风险:

  1. Critical风险2个: SSRF漏洞和限流竞态条件,需立即修复
  2. High风险2个: panic风险和请求体大小限制,建议尽快修复
  3. Medium风险4个: 响应验证、文件权限、依赖锁定、Nonce机制
  4. Low/Info风险2个: 序列化配置和文档完善

建议优先级:

  1. 立即修复F-01(SSRF)和F-02(限流竞态)
  2. 一周内修复所有High风险问题
  3. 两周内完成Medium风险修复
  4. 持续改进Low/Info风险项

审计师签名: Cline Security Audit 审计日期: 2026-05-11 下次审计建议: 2026-08-11(3个月后)