From 2d3caf2e8263dee1b87ae1696690ddd36de57565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nagy=20K=C3=A1roly=20G=C3=A1briel?= Date: Thu, 30 Apr 2026 13:31:51 +0300 Subject: [PATCH] fix: eliminate request handler race condition and add input validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove global d variable; declare passwordData locally per request, eliminating a data race under concurrent requests and the bug where NoUpper/DenyRepeat were never reset between requests - Add parseParam helper that strictly validates integer fields: absent fields fall back to the configured default, while invalid or out-of-range values return HTTP 400 with a descriptive message - Cap password length at 512 characters to prevent CPU/memory exhaustion - Bump go.mod from 1.14 to 1.25 (minimum maintained release; required for errors.Join used in validation) Signed-off-by: Nagy Károly Gábriel --- go.mod | 2 +- main.go | 87 +++++++++++++++++++++++++++++++++++---------------------- 2 files changed, 54 insertions(+), 35 deletions(-) diff --git a/go.mod b/go.mod index d0d77ae..39b4332 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module git.jpi.io/JPI/ranpass -go 1.14 +go 1.25 diff --git a/main.go b/main.go index f1721b1..49d0793 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "errors" "flag" "fmt" "log" @@ -8,6 +9,29 @@ import ( "strconv" ) +const maxPasswordLength = 512 + +// parseParam parses a positive integer form field. If the field is absent or +// empty, defaultVal is returned. If present but invalid or out of [1, max], +// an error is returned. +func parseParam(r *http.Request, name string, defaultVal, max int) (int, error) { + v := r.FormValue(name) + if v == "" { + return defaultVal, nil + } + n, err := strconv.Atoi(v) + if err != nil { + return 0, fmt.Errorf("%s: must be an integer", name) + } + if n <= 0 { + return 0, fmt.Errorf("%s: must be greater than zero", name) + } + if n > max { + return 0, fmt.Errorf("%s: must be at most %d", name, max) + } + return n, nil +} + type passwordData struct { Length int Digits int @@ -17,11 +41,9 @@ type passwordData struct { Password string } -var d, defaults passwordData +var defaults passwordData func generatePassword(w http.ResponseWriter, r *http.Request) { - var err error - if r.URL.Path != "/" { http.Error(w, "404 not found.", http.StatusNotFound) return @@ -29,56 +51,53 @@ func generatePassword(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": - d.Password, err = generate(defaults.Length, defaults.Digits, defaults.Symbols, false, false) + password, err := generate(defaults.Length, defaults.Digits, defaults.Symbols, false, false) if err != nil { - d.Password = "Error: " + err.Error() w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(d.Password)) + w.Write([]byte("Error: " + err.Error())) return } w.WriteHeader(http.StatusOK) - w.Write([]byte(d.Password)) + w.Write([]byte(password)) case "POST": - if err = r.ParseForm(); err != nil { + var d passwordData + + if err := r.ParseForm(); err != nil { w.WriteHeader(http.StatusUnprocessableEntity) w.Write([]byte(fmt.Sprintf("ParseForm() err: %v", err))) return } - d.Length, err = strconv.Atoi(r.FormValue("length")) - if err != nil { - d.Length = defaults.Length - } - d.Digits, err = strconv.Atoi(r.FormValue("digits")) - if err != nil { - d.Digits = defaults.Digits - } + var err error + var validationErr error - d.Symbols, err = strconv.Atoi(r.FormValue("symbols")) - if err != nil { - d.Symbols = defaults.Symbols - } + d.Length, err = parseParam(r, "length", defaults.Length, maxPasswordLength) + validationErr = errors.Join(validationErr, err) - if r.FormValue("noupper") == "on" { - d.NoUpper = true - } + d.Digits, err = parseParam(r, "digits", defaults.Digits, maxPasswordLength) + validationErr = errors.Join(validationErr, err) - if r.FormValue("denyrepeat") == "on" { - d.DenyRepeat = true - } + d.Symbols, err = parseParam(r, "symbols", defaults.Symbols, maxPasswordLength) + validationErr = errors.Join(validationErr, err) - d.Password, err = generate(d.Length, d.Digits, d.Symbols, d.NoUpper, !d.DenyRepeat) - if err != nil { - d.Password = "Error: " + err.Error() - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(d.Password)) + if validationErr != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(validationErr.Error())) return } - if d.Length == 0 { - d.Password = "Error: password can not have zero length" + + d.NoUpper = r.FormValue("noupper") == "on" + d.DenyRepeat = r.FormValue("denyrepeat") == "on" + + var password string + password, err = generate(d.Length, d.Digits, d.Symbols, d.NoUpper, !d.DenyRepeat) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Error: " + err.Error())) + return } w.WriteHeader(http.StatusOK) - w.Write([]byte(d.Password)) + w.Write([]byte(password)) default: w.WriteHeader(http.StatusBadRequest) w.Write([]byte("Sorry, only GET and POST methods are supported."))