From 4f89f5923256c6ac77b6afa592d207a2c7a4318b Mon Sep 17 00:00:00 2001 From: Aleksandr Trushkin Date: Sun, 28 Jun 2026 04:31:21 +0000 Subject: [PATCH] fix(cluster-3): http hardening (M15/M16/M14/C2) - pagination: clamp per_page to [1,100] and page to >=1 in the parser, guard the TotalPages division against per_page=0 (panic), and clamp the current page to [1,totalPages]; preserves cursor (next-token) mode - middleware: add panic-recovery as the outermost middleware so handler panics return a 500 instead of crashing the process; re-panics http.ErrAbortHandler to keep file serving intact - index: bound the index page query (Limit:200) so it no longer drains the entire courses table in 1000-row batches --- cmd/kuriweb/http.go | 25 +++++++++++++++++++++++++ internal/kurious/ports/http/course.go | 18 +++++++++++++++--- internal/kurious/ports/http/server.go | 10 +++++++++- 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/cmd/kuriweb/http.go b/cmd/kuriweb/http.go index 6bad629..8f3ed42 100644 --- a/cmd/kuriweb/http.go +++ b/cmd/kuriweb/http.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "log/slog" "net/http" "strings" @@ -54,6 +55,7 @@ func setupHTTP(cfg config.HTTP, srv xhttp.Server, log *slog.Logger) *http.Server router := mux.NewRouter() router.Use( + middlewareRecovery(log), middlewareCustomWriterInjector(), mux.CORSMethodMiddleware(router), middlewareLogger(log), @@ -101,6 +103,29 @@ func setupHTTP(cfg config.HTTP, srv xhttp.Server, log *slog.Logger) *http.Server } } +func middlewareRecovery(log *slog.Logger) mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + rec := recover() + if rec == nil { + return + } + if rec == http.ErrAbortHandler { + panic(rec) + } + xcontext.LogWithError( + r.Context(), log, fmt.Errorf("%v", rec), "recovered from panic", + slog.String("method", r.Method), + slog.String("path", r.URL.Path), + ) + http.Error(w, "internal server error", http.StatusInternalServerError) + }() + next.ServeHTTP(w, r) + }) + } +} + func middlewareCustomWriterInjector() mux.MiddlewareFunc { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/kurious/ports/http/course.go b/internal/kurious/ports/http/course.go index db06464..c76cb27 100644 --- a/internal/kurious/ports/http/course.go +++ b/internal/kurious/ports/http/course.go @@ -213,6 +213,15 @@ func (c courseTemplServer) List(w http.ResponseWriter, r *http.Request) { } }) + totalPages := 0 + if pathParams.PerPage > 0 { + totalPages = listCoursesResult.Count / pathParams.PerPage + } + currentPage := pathParams.Page + if currentPage > 0 && totalPages > 0 && currentPage > totalPages { + currentPage = totalPages + } + params = bootstrap.ListCoursesParams{ FilterForm: bootstrap.FilterFormParams{ Render: true, @@ -233,8 +242,8 @@ func (c courseTemplServer) List(w http.ResponseWriter, r *http.Request) { Courses: params.Courses, Categories: params.Categories, Pagination: bootstrap.Pagination{ - Page: pathParams.Page, - TotalPages: listCoursesResult.Count / pathParams.PerPage, + Page: currentPage, + TotalPages: totalPages, BaseURL: r.URL.Path, }, } @@ -285,7 +294,10 @@ func (c courseTemplServer) Index(w http.ResponseWriter, r *http.Request) { stats := bootstrap.MakeNewStats(1, 2, 3) - coursesResult, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{}) + const indexCoursesLimit = 200 + coursesResult, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{ + Limit: indexCoursesLimit, + }) if handleError(ctx, err, w, c.log, "unable to list courses") { return } diff --git a/internal/kurious/ports/http/server.go b/internal/kurious/ports/http/server.go index 9bde71f..10b5243 100644 --- a/internal/kurious/ports/http/server.go +++ b/internal/kurious/ports/http/server.go @@ -88,14 +88,22 @@ func parsePaginationFromQuery(r *http.Request) (out pagination, err error) { } else { out.PerPage = 20 } + if out.PerPage < 1 { + out.PerPage = 1 + } else if out.PerPage > 100 { + out.PerPage = 100 + } if query.Has("page") { out.Page, err = strconv.Atoi(query.Get("page")) if err != nil { - return out, errors.NewValidationError("page", "bad per_page value") + return out, errors.NewValidationError("page", "bad page value") } } else if !query.Has("next") { out.Page = 1 } + if out.Page < 1 && !query.Has("next") { + out.Page = 1 + } return out, nil }