diff --git a/internal/handlers/app.go b/internal/handlers/app.go index b98eedc..f60435c 100644 --- a/internal/handlers/app.go +++ b/internal/handlers/app.go @@ -373,6 +373,28 @@ func (h *Handlers) HandleAppDeployments() http.HandlerFunc { // defaultLogTail is the default number of log lines to fetch. const defaultLogTail = "500" +// maxLogTail is the maximum allowed value for the tail parameter. +const maxLogTail = 500 + +// sanitizeTail validates and clamps the tail query parameter. +// It returns a numeric string clamped to maxLogTail, or the default if invalid. +func sanitizeTail(raw string) string { + if raw == "" { + return defaultLogTail + } + + n, err := strconv.Atoi(raw) + if err != nil || n < 1 { + return defaultLogTail + } + + if n > maxLogTail { + n = maxLogTail + } + + return strconv.Itoa(n) +} + // HandleAppLogs returns the container logs handler. func (h *Handlers) HandleAppLogs() http.HandlerFunc { return func(writer http.ResponseWriter, request *http.Request) { @@ -394,10 +416,7 @@ func (h *Handlers) HandleAppLogs() http.HandlerFunc { return } - tail := request.URL.Query().Get("tail") - if tail == "" { - tail = defaultLogTail - } + tail := sanitizeTail(request.URL.Query().Get("tail")) logs, logsErr := h.docker.ContainerLogs( request.Context(), diff --git a/internal/handlers/tail_validation_test.go b/internal/handlers/tail_validation_test.go new file mode 100644 index 0000000..0ec0d7a --- /dev/null +++ b/internal/handlers/tail_validation_test.go @@ -0,0 +1,38 @@ +package handlers + +import ( + "testing" +) + +func TestSanitizeTail(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + expected string + }{ + {"empty uses default", "", defaultLogTail}, + {"valid small number", "50", "50"}, + {"valid max boundary", "500", "500"}, + {"exceeds max clamped", "501", "500"}, + {"very large clamped", "999999", "500"}, + {"non-numeric uses default", "abc", defaultLogTail}, + {"all keyword uses default", "all", defaultLogTail}, + {"negative uses default", "-1", defaultLogTail}, + {"zero uses default", "0", defaultLogTail}, + {"float uses default", "1.5", defaultLogTail}, + {"one is valid", "1", "1"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + got := sanitizeTail(tc.input) + if got != tc.expected { + t.Errorf("sanitizeTail(%q) = %q, want %q", tc.input, got, tc.expected) + } + }) + } +}