From cfa49758139705c25b4d3dfbf25050a6cc20c8b0 Mon Sep 17 00:00:00 2001 From: xfy Date: Mon, 15 Jun 2026 11:27:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(posts):=20=E9=92=B3=E5=88=B6=E5=88=86?= =?UTF-8?q?=E9=A1=B5=E5=8F=82=E6=95=B0=20per=5Fpage=20=E4=B8=8A=E9=99=90?= =?UTF-8?q?=EF=BC=8C=E6=B6=88=E9=99=A4=E5=85=AC=E5=BC=80=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=20DoS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit list_published_posts 为无需认证的公开接口,原先 per_page 直接透传给 SQL LIMIT,攻击者可传入巨大值迫使数据库扫描并实例化超大 Vec,造成 内存放大与拒绝服务。新增 MAX_PER_PAGE=50 与 clamp_pagination(),在 list_published_posts 与 list_posts 入口钳制 page/per_page;钳制值同时 用于缓存键与查询,避免同一逻辑页落入不同缓存条目。 --- src/api/posts/list.rs | 53 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/api/posts/list.rs b/src/api/posts/list.rs index 73b4260..ab31697 100644 --- a/src/api/posts/list.rs +++ b/src/api/posts/list.rs @@ -14,6 +14,19 @@ use super::types::PostListResponse; use crate::api::error::AppError; use crate::db::pool::get_conn; +/// 单页允许的最大文章数。 +/// +/// 公开的 `list_published_posts` 接口无需认证,若不对 `per_page` 设上限, +/// 攻击者可传入巨大值迫使数据库扫描并实例化超大 Vec,造成内存放大与拒绝服务。 +const MAX_PER_PAGE: i32 = 50; + +/// 将分页参数钳制到安全范围:页码至少为 1,每页 1–`MAX_PER_PAGE`。 +/// +/// 注意:返回值必须同时用于缓存键与 SQL 查询,避免同一逻辑页落入不同缓存条目。 +fn clamp_pagination(page: i32, per_page: i32) -> (i32, i32) { + (page.max(1), per_page.clamp(1, MAX_PER_PAGE)) +} + /// 获取已发布文章分页列表。 /// /// 优先命中缓存;未命中时查询总数与分页记录,并按 published_at 降序排列。 @@ -22,6 +35,9 @@ pub async fn list_published_posts( page: i32, per_page: i32, ) -> Result { + // 钳制分页参数,防止无认证调用方请求超大每页数量导致内存放大 / DoS。 + let (page, per_page) = clamp_pagination(page, per_page); + #[cfg(feature = "server")] { let cache_key = crate::cache::CacheKey::PublishedPosts { page, per_page }; @@ -93,6 +109,8 @@ pub async fn list_published_posts( /// 需要 admin 权限;结果按创建时间降序,不走缓存。 #[server(ListPosts, "/api")] pub async fn list_posts(page: i32, per_page: i32) -> Result { + // 与公开接口保持一致的分页钳制,避免单次请求拉取过多记录。 + let (page, per_page) = clamp_pagination(page, per_page); let _user = get_current_admin_user().await?; #[cfg(feature = "server")] @@ -199,3 +217,38 @@ pub async fn get_posts_by_tag(tag_name: String) -> Result