xfy 8f288c60da fix(comments): rate-limit check_pending_status to prevent status enumeration
该接口供访客轮询自己刚提交评论的审核状态,故不加 admin 鉴权(会破坏合法
轮询);改为加 strict 限流(对 unknown IP 降级宽松桶),阻止批量枚举评论
状态(L3)。复用 check_strict_limit,与其它敏感接口一致。
2026-06-18 13:43:57 +08:00

77 lines
2.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 评论审核状态批量查询。
//!
//! 用于前端轮询刚刚提交的评论是否已通过审核,支持传入多个 id。
//! 仅在 `feature = "server"` 启用的服务端构建中查询数据库。
use dioxus::prelude::*;
/// 单个评论的待审核状态结果。
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PendingStatusItem {
/// 评论 id。
pub id: i64,
/// 当前状态approved / pending / rejected / spam / trash / gone。
pub status: String,
}
/// 查询一组评论的当前审核状态。
///
/// 如果某 id 不存在,则返回状态 `"gone"`。
/// Dioxus server function注册在 `/api` 路径下。
#[server(CheckPendingStatus, "/api")]
pub async fn check_pending_status(ids: Vec<i64>) -> Result<Vec<PendingStatusItem>, ServerFnError> {
// 仅在服务端构建中执行 SQL 查询。
#[cfg(feature = "server")]
{
use crate::api::error::AppError;
use crate::db::pool::get_conn;
// 空列表直接返回空结果,避免无意义的数据库查询。
if ids.is_empty() {
return Ok(vec![]);
}
// 限流防高速遍历枚举评论状态L3。本接口供访客轮询自己刚提交的评论
// 审核状态,故不加 admin 鉴权;但 strict 限流(对 unknown IP 降级宽松桶)
// 足以阻止批量枚举。
if let Some(ctx) = dioxus::fullstack::FullstackContext::current() {
let parts = ctx.parts_mut();
let ip = crate::api::rate_limit::get_client_ip(&parts.headers);
if let Err(_msg) = crate::api::rate_limit::check_strict_limit(&ip) {
return Err(ServerFnError::new("请求过于频繁,请稍后再试"));
}
}
let client = get_conn().await.map_err(AppError::db_conn)?;
let rows = client
.query(
"SELECT id, status FROM comments WHERE id = ANY($1)",
&[&ids],
)
.await
.map_err(AppError::query)?;
// 将查询结果收集为 HashMap便于后续按传入顺序补齐缺失项。
let found: std::collections::HashMap<i64, String> = rows
.iter()
.map(|r| (r.get::<_, i64>(0), r.get::<_, String>(1)))
.collect();
let result: Vec<PendingStatusItem> = ids
.into_iter()
.map(|id| {
let status = found
.get(&id)
.cloned()
.unwrap_or_else(|| "gone".to_string());
PendingStatusItem { id, status }
})
.collect();
Ok(result)
}
#[cfg(not(feature = "server"))]
unreachable!()
}