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));