fix(billing): resolve all audit findings — CSRF, float precision, TOCTOU, error sanitization
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

- Add CSRF token protection for mock payment (SHA256 + constant-time verify)
- Replace f64 currency conversion with pure integer string parsing (parse_yuan_to_cents)
- Move subscription check inside transaction to prevent TOCTOU race
- Rewrite increment_usage to use atomic SQL (account_id+period_start WHERE)
- Add trade_no format validation in payment callback
- Sanitize error messages to prevent sensitive data leakage
- Use i32::try_from for WeChat amount conversion (prevent truncation)
- Replace window.__ZCLAW_STATS_SYNC_INTERVAL__ with useRef pattern
- Replace eprintln/println with tracing macros in lifecycle
- Remove unused variable in scheduler
- Remove duplicate sha2 and unused hmac from Cargo.toml
This commit is contained in:
iven
2026-04-02 20:04:43 +08:00
parent 8898bb399e
commit da438ad868
7 changed files with 127 additions and 34 deletions

View File

@@ -21,7 +21,11 @@ pub async fn create_payment(
req: &CreatePaymentRequest,
config: &PaymentConfig,
) -> SaasResult<PaymentResult> {
// 1. 获取计划信息
// 1. 在事务中完成所有检查和创建
let mut tx = pool.begin().await
.map_err(|e| SaasError::Internal(format!("开启事务失败: {}", e)))?;
// 1a. 获取计划信息(事务内)
let plan = sqlx::query_as::<_, BillingPlan>(
"SELECT * FROM billing_plans WHERE id = $1 AND status = 'active'"
)
@@ -30,7 +34,7 @@ pub async fn create_payment(
.await?
.ok_or_else(|| SaasError::NotFound("计划不存在或已下架".into()))?;
// 检查是否已有活跃订阅
// 1b. 检查是否已有活跃订阅(事务内,防并发重复)
let existing = sqlx::query_scalar::<_, i64>(
"SELECT COUNT(*) FROM billing_subscriptions \
WHERE account_id = $1 AND status IN ('trial', 'active') AND plan_id = $2"
@@ -44,10 +48,6 @@ pub async fn create_payment(
return Err(SaasError::InvalidInput("已订阅该计划".into()));
}
// 2. 在事务中创建发票和支付记录
let mut tx = pool.begin().await
.map_err(|e| SaasError::Internal(format!("开启事务失败: {}", e)))?;
let invoice_id = uuid::Uuid::new_v4().to_string();
let now = chrono::Utc::now();
let due = now + chrono::Duration::days(1);
@@ -465,7 +465,7 @@ async fn generate_wechat_url(
}
let resp_json: serde_json::Value = resp.json().await
.map_err(|e| SaasError::Internal(format!("微信支付响应解析失败: {}", e)))?;
.map_err(|e| SaasError::Internal("微信支付响应解析失败".into()))?;
let code_url = resp_json.get("code_url")
.and_then(|v| v.as_str())
@@ -549,7 +549,7 @@ pub fn decrypt_wechat_resource(
msg: &ciphertext,
aad: associated_data.as_bytes(),
})
.map_err(|e| SaasError::Internal(format!("AES-GCM 解密失败: {}", e)))?;
.map_err(|_| SaasError::Internal("AES-GCM 解密失败".into()))?;
String::from_utf8(plaintext)
.map_err(|e| SaasError::Internal(format!("解密结果 UTF-8 转换失败: {}", e)))