- 审计日期: 2026-05-11
- 审计范围: 全部源代码、配置文件、部署脚本
- 审计工具: 静态代码分析、手动审查
| 编号 | 严重程度 | 标题 | 位置 | 状态 |
|---|---|---|---|---|
| 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/ | 未修复 |
位置: 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)攻击场景:
- 攻击者设置
UPSTREAM_URL=http://169.254.169.254/latest/meta-data/ - 网关向AWS元数据服务发起请求
- 获取IAM凭证等敏感信息
修复建议:
- 新增环境变量
ALLOWED_UPSTREAM_HOSTS(逗号分隔白名单域名) - 在
forward_to_upstream函数中验证URL host是否在白名单内 - 拒绝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
位置: 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
}
}
// ...
}攻击场景:
- 攻击者使用100个并发请求
- 所有请求同时读取
remaining=1 - 所有请求都通过检查,实际发送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
位置: 多个文件(共27处)
描述:
代码中大量使用unwrap()和expect(),当遇到无效输入或外部数据异常时会导致进程panic,造成拒绝服务。
关键风险点:
- 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();- 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")
}- erc-store/src/lib.rs:17-23 - 数据库锁panic
let conn = self.conn.lock().unwrap(); // 锁被污染时panic修复建议:
- 将所有
unwrap()替换为?操作符或显式错误处理 - 对于网络绑定,返回有意义的错误信息
- 对于数据库操作,使用
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())?;位置: 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
位置: 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)
})?;修复建议:
- 定义上游响应的JSON Schema
- 使用
jsonschemacrate验证响应结构 - 拒绝包含意外字段的响应
// 建议修复代码
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(", "))
})
}位置: 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("无法打开限流数据库");修复建议:
- 在Unix系统上使用
std::os::unix::fs::PermissionsExt设置权限 - 在Windows上使用ACL限制访问
- 创建后立即设置权限
// 建议修复代码
#[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
位置: 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位置: 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,
);修复建议:
- 使用SQLite的
UPSERT RETURNING实现原子递增 - 基于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
位置: 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")
}位置: docs/
描述: 文档目录缺少安全配置指南,包括:
- Token生成和管理
- 网络隔离建议
- 文件权限设置
- 监控和告警配置
修复建议:
创建docs/SECURITY.md文档,包含:
- 生产环境部署检查清单
- Token轮换策略
- 网络拓扑建议
- 安全监控指标
需要运行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✅ 无unsafe代码 - 项目未使用任何unsafe块,符合Rust安全最佳实践。
| 文件 | 行号 | 代码 | 风险等级 |
|---|---|---|---|
| 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 |
- 设置
ERC_API_TOKEN环境变量(强制) - 设置
ALLOWED_UPSTREAM_HOSTS白名单(待实现) - 设置
ERC_CORS_ORIGIN限制跨域访问 - 数据库文件权限设置为600
- 使用非特权用户运行进程
- 启用HTTPS(生产环境)
- 配置日志不包含敏感信息
- 定期运行
cargo audit
ERC Framework在安全设计上有良好基础(无unsafe代码、使用ed25519签名、强制API Token),但存在以下关键风险:
- Critical风险2个: SSRF漏洞和限流竞态条件,需立即修复
- High风险2个: panic风险和请求体大小限制,建议尽快修复
- Medium风险4个: 响应验证、文件权限、依赖锁定、Nonce机制
- Low/Info风险2个: 序列化配置和文档完善
建议优先级:
- 立即修复F-01(SSRF)和F-02(限流竞态)
- 一周内修复所有High风险问题
- 两周内完成Medium风险修复
- 持续改进Low/Info风险项
审计师签名: Cline Security Audit 审计日期: 2026-05-11 下次审计建议: 2026-08-11(3个月后)