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:
@@ -1,8 +1,13 @@
|
||||
use axum::Json;
|
||||
use axum::extract::Extension;
|
||||
use serde::Deserialize;
|
||||
use tracing;
|
||||
|
||||
use erp_core::types::ApiResponse;
|
||||
use erp_core::error::AppError;
|
||||
use erp_core::rbac::require_permission;
|
||||
use erp_core::types::{ApiResponse, TenantContext};
|
||||
|
||||
const MAX_EVENTS_PER_BATCH: usize = 100;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)] // 客户端上报结构体,字段后续接入分析表时使用
|
||||
@@ -37,7 +42,17 @@ pub struct BatchRequest {
|
||||
|
||||
/// 接收小程序批量埋点事件。
|
||||
/// 当前为日志记录模式 — 后续可接入 ClickHouse/PostgreSQL 分析表。
|
||||
pub async fn batch(Json(req): Json<BatchRequest>) -> Json<ApiResponse<()>> {
|
||||
pub async fn batch(
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Json(req): Json<BatchRequest>,
|
||||
) -> Result<Json<ApiResponse<()>>, AppError> {
|
||||
require_permission(&ctx, "system.analytics.submit")?;
|
||||
if req.events.len() > MAX_EVENTS_PER_BATCH {
|
||||
return Err(AppError::Validation(format!(
|
||||
"批量埋点事件数不能超过 {} 条",
|
||||
MAX_EVENTS_PER_BATCH
|
||||
)));
|
||||
}
|
||||
for evt in &req.events {
|
||||
tracing::info!(
|
||||
event = %evt.event,
|
||||
@@ -46,5 +61,5 @@ pub async fn batch(Json(req): Json<BatchRequest>) -> Json<ApiResponse<()>> {
|
||||
"Analytics event received"
|
||||
);
|
||||
}
|
||||
Json(ApiResponse::ok(()))
|
||||
Ok(Json(ApiResponse::ok(())))
|
||||
}
|
||||
|
||||
@@ -921,6 +921,22 @@ async fn security_headers_middleware(
|
||||
header::HeaderName::from_static("referrer-policy"),
|
||||
HeaderValue::from_static("strict-origin-when-cross-origin"),
|
||||
);
|
||||
headers.insert(
|
||||
header::STRICT_TRANSPORT_SECURITY,
|
||||
HeaderValue::from_static("max-age=63072000; includeSubDomains; preload"),
|
||||
);
|
||||
headers.insert(
|
||||
header::HeaderName::from_static("content-security-policy"),
|
||||
HeaderValue::from_static(
|
||||
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; \
|
||||
img-src 'self' data: blob: https:; connect-src 'self' wss:; \
|
||||
frame-ancestors 'none'; base-uri 'self'; form-action 'self'",
|
||||
),
|
||||
);
|
||||
headers.insert(
|
||||
header::HeaderName::from_static("permissions-policy"),
|
||||
HeaderValue::from_static("camera=(), microphone=(), geolocation=(), payment=()"),
|
||||
);
|
||||
response
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user