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

@@ -1,9 +1,9 @@
use std::cell::Cell;
use std::collections::HashSet;
use std::convert::Infallible;
use axum::extract::{Extension, Query};
use axum::http::HeaderMap;
use axum::http::{HeaderMap, HeaderValue, header};
use axum::response::IntoResponse;
use axum::response::sse::{Event, KeepAlive, Sse};
use futures::stream::Stream;
use sea_orm::ConnectionTrait;
@@ -13,6 +13,23 @@ use uuid::Uuid;
use erp_core::error::AppError;
use erp_core::types::TenantContext;
/// 包装 SSE 响应,添加 Cache-Control: no-store 头
pub struct NoCacheSse<S>(Sse<S>);
impl<S> IntoResponse for NoCacheSse<S>
where
S: Stream<Item = Result<Event, std::convert::Infallible>> + Send + 'static,
{
fn into_response(self) -> axum::response::Response {
let mut response = self.0.into_response();
response.headers_mut().insert(
header::CACHE_CONTROL,
HeaderValue::from_static("no-store, no-cache, must-revalidate"),
);
response
}
}
use crate::message_state::MessageState;
/// SSE 查询参数
@@ -38,7 +55,7 @@ pub async fn message_stream(
Extension(ctx): Extension<TenantContext>,
headers: HeaderMap,
Query(query): Query<SseQuery>,
) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, AppError> {
) -> Result<NoCacheSse<impl Stream<Item = Result<Event, std::convert::Infallible>>>, AppError> {
let user_id = ctx.user_id;
let tenant_id = ctx.tenant_id;
@@ -165,10 +182,12 @@ pub async fn message_stream(
}
};
Ok(Sse::new(sse_stream).keep_alive(
KeepAlive::new()
.interval(std::time::Duration::from_secs(30))
.text("ping"),
Ok(NoCacheSse(
Sse::new(sse_stream).keep_alive(
KeepAlive::new()
.interval(std::time::Duration::from_secs(30))
.text("ping"),
),
))
}