From eaa7118e0922ddcea343c54370c0605b3e8b0fda Mon Sep 17 00:00:00 2001 From: xfy Date: Mon, 15 Jun 2026 11:38:24 +0800 Subject: [PATCH] =?UTF-8?q?fix(posts):=20=E9=92=B3=E5=88=B6=20page=20?= =?UTF-8?q?=E4=B8=8A=E9=99=90=EF=BC=8C=E9=98=B2=E6=AD=A2=E6=97=A0=E7=95=8C?= =?UTF-8?q?=20OFFSET=20=E4=B8=8E=E7=BC=93=E5=AD=98=E9=94=AE=E6=89=87?= =?UTF-8?q?=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit clamp_pagination 原先仅限制 per_page,page 无上限。攻击者可用海量不同 page 值撑大缓存键空间(缓存污染)并触发无意义的超大 OFFSET 扫描。新增 MAX_PAGE=10_000,对任何实际博客都足够宽裕(配合 MAX_PER_PAGE 最多覆盖 50 万篇文章),同时把缓存键空间限制在有限范围。 --- src/api/posts/list.rs | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/api/posts/list.rs b/src/api/posts/list.rs index ab31697..a7e740d 100644 --- a/src/api/posts/list.rs +++ b/src/api/posts/list.rs @@ -20,11 +20,21 @@ use crate::db::pool::get_conn; /// 攻击者可传入巨大值迫使数据库扫描并实例化超大 Vec,造成内存放大与拒绝服务。 const MAX_PER_PAGE: i32 = 50; -/// 将分页参数钳制到安全范围:页码至少为 1,每页 1–`MAX_PER_PAGE`。 +/// 允许的最大页码。 +/// +/// `page` 无上限时,攻击者可用海量不同 `page` 值撑大缓存键空间(缓存污染), +/// 并触发无意义的超大 `OFFSET` 扫描。10_000 对任何实际博客都足够宽裕 +/// (配合 `MAX_PER_PAGE` 最多覆盖 50 万篇文章),同时把缓存键空间限制在有限范围。 +const MAX_PAGE: i32 = 10_000; + +/// 将分页参数钳制到安全范围:页码 1–`MAX_PAGE`,每页 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)) + ( + page.clamp(1, MAX_PAGE), + per_page.clamp(1, MAX_PER_PAGE), + ) } /// 获取已发布文章分页列表。 @@ -243,6 +253,19 @@ mod tests { assert_eq!(clamp_pagination(1, -100), (1, 1)); } + #[test] + fn clamp_pagination_clamps_oversized_page() { + // 巨大 page 必须被压回上限,避免无界 OFFSET 扫描与缓存键扇出。 + assert_eq!(clamp_pagination(i32::MAX, 10), (MAX_PAGE, 10)); + assert_eq!(clamp_pagination(MAX_PAGE + 1, 10), (MAX_PAGE, 10)); + } + + #[test] + fn clamp_pagination_max_page_boundary() { + assert_eq!(clamp_pagination(MAX_PAGE, 10), (MAX_PAGE, 10)); + assert_eq!(clamp_pagination(MAX_PAGE - 1, 10), (MAX_PAGE - 1, 10)); + } + #[test] fn clamp_pagination_max_per_page_boundary() { assert_eq!(clamp_pagination(1, MAX_PER_PAGE), (1, MAX_PER_PAGE));