fix(security): 安全加固 — analytics 权限校验 + HSTS/CSP 安全头 + SSE no-cache + SQL 参数化

- analytics batch() 添加 require_permission + 事件数上限 100
- main.rs 添加 HSTS/Content-Security-Policy/Permissions-Policy 安全头
- sse_handler SSE 响应添加 Cache-Control: no-store 防 token 泄漏
- action_inbox_service SQL 查询改为参数化,防注入
- wechat_handler 日志脱敏,不打印 appid/secret 长度
- dynamic_table sanitize_identifier 添加 63 字节限制
This commit is contained in:
iven
2026-05-20 17:52:28 +08:00
parent fa1dc764a3
commit 65cf96f119
6 changed files with 130 additions and 28 deletions

View File

@@ -237,31 +237,72 @@ pub async fn list_action_items(
let filter_by_user = query.assigned_to_me.unwrap_or(false) && user_id.is_some();
// 各段的 status 过滤条件
// 各段的 status 过滤条件(参数化)
let (sug_status, alert_status, fu_status) = match query.status.as_deref() {
Some("pending") => (
"AND s.status = 'pending'".into(),
"AND al.status = 'active'".into(),
"AND f.status = 'pending'".into(),
"AND s.status = $6".to_string(),
"AND al.status = $7".to_string(),
"AND f.status = $8".to_string(),
),
Some("in_progress") => (
"AND s.status = 'approved'".into(),
"AND al.status = 'acknowledged'".into(),
"AND f.status = 'in_progress'".into(),
"AND s.status = $6".to_string(),
"AND al.status = $7".to_string(),
"AND f.status = $8".to_string(),
),
Some("completed") => (
"AND s.status = 'executed'".into(),
"AND al.status = 'resolved'".into(),
"AND f.status = 'completed'".into(),
"AND s.status = $6".to_string(),
"AND al.status = $7".to_string(),
"AND f.status = $8".to_string(),
),
Some("dismissed") => (
"AND s.status IN ('rejected', 'expired', 'parse_failed')".into(),
"AND al.status IN ('dismissed', 'expired')".into(),
"AND f.status IN ('cancelled', 'skipped')".into(),
"AND s.status = ANY($6)".to_string(),
"AND al.status = ANY($7)".to_string(),
"AND f.status = ANY($8)".to_string(),
),
_ => (String::new(), String::new(), String::new()),
};
// status 绑定值
let (sug_val, alert_val, fu_val): (sea_orm::Value, sea_orm::Value, sea_orm::Value) =
match query.status.as_deref() {
Some("pending") => ("pending".into(), "active".into(), "pending".into()),
Some("in_progress") => (
"approved".into(),
"acknowledged".into(),
"in_progress".into(),
),
Some("completed") => ("executed".into(), "resolved".into(), "completed".into()),
Some("dismissed") => (
sea_orm::Value::Array(
sea_orm::sea_query::ArrayType::String,
Some(Box::new(vec![
sea_orm::Value::String(Some(Box::new("rejected".to_string()))),
sea_orm::Value::String(Some(Box::new("expired".to_string()))),
sea_orm::Value::String(Some(Box::new("parse_failed".to_string()))),
])),
),
sea_orm::Value::Array(
sea_orm::sea_query::ArrayType::String,
Some(Box::new(vec![
sea_orm::Value::String(Some(Box::new("dismissed".to_string()))),
sea_orm::Value::String(Some(Box::new("expired".to_string()))),
])),
),
sea_orm::Value::Array(
sea_orm::sea_query::ArrayType::String,
Some(Box::new(vec![
sea_orm::Value::String(Some(Box::new("cancelled".to_string()))),
sea_orm::Value::String(Some(Box::new("skipped".to_string()))),
])),
),
),
_ => (
sea_orm::Value::String(None),
sea_orm::Value::String(None),
sea_orm::Value::String(None),
),
};
// 按类型过滤
let type_filter = query.action_type.as_deref();
let include_sug = type_filter.is_none_or(|t| t == "ai_suggestion");
@@ -335,7 +376,7 @@ pub async fn list_action_items(
let union_sql = segments.join("\n UNION ALL\n");
// $1=tenant_id, $2=patient_id, $3=assigned_to (union 内部)
// $1=tenant_id, $2=patient_id, $3=assigned_to, $6/$7/$8=status (union 内部)
// $4=LIMIT, $5=OFFSET (外层分页)
let patient_val: sea_orm::Value = query
.patient_id
@@ -362,6 +403,9 @@ pub async fn list_action_items(
assigned_val.clone(),
(page_size as i64).into(),
(offset as i64).into(),
sug_val.clone(),
alert_val.clone(),
fu_val.clone(),
],
))
.all(db)
@@ -375,7 +419,14 @@ pub async fn list_action_items(
FromQueryResult::find_by_statement(Statement::from_sql_and_values(
DatabaseBackend::Postgres,
count_sql,
[tenant_id.into(), patient_val, assigned_val],
[
tenant_id.into(),
patient_val,
assigned_val,
sug_val,
alert_val,
fu_val,
],
))
.one(db)
.await