From 59c37a8fea46e34bdad5909370ba70b8e9bfaadf Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Tue, 27 Oct 2026 15:33:21 +0100 Subject: [PATCH] fix: a huge export or list page answers an empty page instead of panicking (WR-14) (page-1)*perPage overflowed for a large page and sliced out of range. Export compares the page with the page count before multiplying, and the list endpoints cap the page number. --- classes/transfer.go | 8 +++++--- controllers/api/auth.go | 8 +++++++- transfer_test.go | 10 ++++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/classes/transfer.go b/classes/transfer.go index eeafdf2..3fa61d3 100644 --- a/classes/transfer.go +++ b/classes/transfer.go @@ -160,9 +160,11 @@ func (s *Service) Export(ctx context.Context, dataset, delimiter string, page, p page = 1 } out.Page = page - start := (page - 1) * perPage - if start > len(rows) { - start = len(rows) + // A page past the last is empty; compared before multiplying, so a + // huge page cannot overflow into a negative start (WR-14). + start := len(rows) + if page <= out.Pages { + start = (page - 1) * perPage } end := min(start+perPage, len(rows)) rows = rows[start:end] diff --git a/controllers/api/auth.go b/controllers/api/auth.go index 4a15c72..dd2887f 100644 --- a/controllers/api/auth.go +++ b/controllers/api/auth.go @@ -184,10 +184,12 @@ func read(ctx context.Context, svc *classes.Service) (*gorm.DB, error) { return svc.DB(ctx) } -// The page sizes of the list endpoints. +// The page sizes of the list endpoints, and the highest page number read +// (a later page is empty anyway). const ( defaultPerPage = 50 maxPerPage = 200 + maxPage = 1_000_000 ) // ListInput is the paging and search query every list tool shares. @@ -206,6 +208,10 @@ func listParams(r *http.Request) ListInput { if in.Page < 1 { in.Page = 1 } + // (Page-1)*PerPage must not overflow into a negative offset (WR-14). + if in.Page > maxPage { + in.Page = maxPage + } if in.PerPage < 1 { in.PerPage = defaultPerPage } diff --git a/transfer_test.go b/transfer_test.go index b430464..2fea39c 100644 --- a/transfer_test.go +++ b/transfer_test.go @@ -73,6 +73,16 @@ func TestCSVTransfer(t *testing.T) { t.Fatalf("filename = %v", exp["filename"]) } + // A huge page number is an empty page, not a panic (WR-14); so is a + // list page. + far := env.mcpExpect(t, http.StatusOK, http.MethodGet, "/_seo/api/v1/export?dataset=redirects&page=9223372036854777", agent, nil) + if c, _ := far["csv"].(string); strings.Contains(c, "/e1") { + t.Fatalf("export of a huge page = %v, want no rows", far) + } + if l := env.mcpExpect(t, http.StatusOK, http.MethodGet, "/_seo/api/v1/redirects?page=9223372036854775807&per_page=200", agent, nil); len(l["data"].([]any)) != 0 { + t.Fatalf("list of a huge page = %v, want no rows", l) + } + preview := env.mcpExpect(t, http.StatusOK, http.MethodPost, "/_seo/api/v1/import", agent, map[string]any{"dataset": "redirects", "csv": csv, "delimiter": ";", "dry_run": true}) if s := summary(t, preview); s["unchanged"] != 2 || s["new"] != 0 || s["changed"] != 0 || s["refused"] != 0 { t.Fatalf("re-import of the export = %v, want 2 unchanged", preview)