security(server): clamp pagination offset before iterator traversal

CodeQL's rust/uncontrolled-allocation-size alert flagged `paginate_items`
and the models list handler because `PaginationParams.offset: u32` was
cast to `usize` without an upper bound and handed to `Iterator::skip`.
In practice the underlying stores are bounded and `skip` on a Vec
iterator is O(1), so the existing callers couldn't be coerced into
allocating arbitrary memory, but an unbounded `offset` still takes an
unbounded time to walk past and CodeQL had no way to see that.

Clamp `offset` to `MAX_PAGE_OFFSET = 1_000_000` (beyond our largest
expected run count by several orders of magnitude) in both the shared
`paginate_items` helper and the models list handler that rolls its own
pagination. `limit` was already clamped to 100.

Closes code-scanning alert #27.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-04-19 15:17:40 -04:00
parent ec33c6a0ea
commit 9ddf6c06be
No known key found for this signature in database

View file

@ -2711,9 +2711,11 @@ async fn board_run_metadata(
metadata
}
const MAX_PAGE_OFFSET: u32 = 1_000_000;
fn paginate_items<T>(items: Vec<T>, pagination: &PaginationParams) -> (Vec<T>, bool) {
let limit = pagination.limit.clamp(1, 100) as usize;
let offset = pagination.offset as usize;
let offset = pagination.offset.min(MAX_PAGE_OFFSET) as usize;
let mut data: Vec<_> = items.into_iter().skip(offset).take(limit + 1).collect();
let has_more = data.len() > limit;
data.truncate(limit);
@ -6326,7 +6328,7 @@ async fn list_models(
let query = params.query.as_ref().map(|value| value.to_lowercase());
let limit = params.limit.clamp(1, 100) as usize;
let offset = params.offset as usize;
let offset = params.offset.min(MAX_PAGE_OFFSET) as usize;
let mut models = fabro_model::Catalog::builtin()
.list(provider)