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
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:
@@ -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)))
|
||||
|
||||
Reference in New Issue
Block a user