fix: eliminate request handler race condition and add input validation
- 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 <k@jpi.io>
This commit is contained in:
@@ -1,3 +1,3 @@
|
|||||||
module git.jpi.io/JPI/ranpass
|
module git.jpi.io/JPI/ranpass
|
||||||
|
|
||||||
go 1.14
|
go 1.25
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
@@ -8,6 +9,29 @@ import (
|
|||||||
"strconv"
|
"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 {
|
type passwordData struct {
|
||||||
Length int
|
Length int
|
||||||
Digits int
|
Digits int
|
||||||
@@ -17,11 +41,9 @@ type passwordData struct {
|
|||||||
Password string
|
Password string
|
||||||
}
|
}
|
||||||
|
|
||||||
var d, defaults passwordData
|
var defaults passwordData
|
||||||
|
|
||||||
func generatePassword(w http.ResponseWriter, r *http.Request) {
|
func generatePassword(w http.ResponseWriter, r *http.Request) {
|
||||||
var err error
|
|
||||||
|
|
||||||
if r.URL.Path != "/" {
|
if r.URL.Path != "/" {
|
||||||
http.Error(w, "404 not found.", http.StatusNotFound)
|
http.Error(w, "404 not found.", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
@@ -29,56 +51,53 @@ func generatePassword(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case "GET":
|
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 {
|
if err != nil {
|
||||||
d.Password = "Error: " + err.Error()
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
w.Write([]byte(d.Password))
|
w.Write([]byte("Error: " + err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write([]byte(d.Password))
|
w.Write([]byte(password))
|
||||||
case "POST":
|
case "POST":
|
||||||
if err = r.ParseForm(); err != nil {
|
var d passwordData
|
||||||
|
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||||
w.Write([]byte(fmt.Sprintf("ParseForm() err: %v", err)))
|
w.Write([]byte(fmt.Sprintf("ParseForm() err: %v", err)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
d.Length, err = strconv.Atoi(r.FormValue("length"))
|
|
||||||
if err != nil {
|
|
||||||
d.Length = defaults.Length
|
|
||||||
}
|
|
||||||
|
|
||||||
d.Digits, err = strconv.Atoi(r.FormValue("digits"))
|
var err error
|
||||||
if err != nil {
|
var validationErr error
|
||||||
d.Digits = defaults.Digits
|
|
||||||
}
|
|
||||||
|
|
||||||
d.Symbols, err = strconv.Atoi(r.FormValue("symbols"))
|
d.Length, err = parseParam(r, "length", defaults.Length, maxPasswordLength)
|
||||||
if err != nil {
|
validationErr = errors.Join(validationErr, err)
|
||||||
d.Symbols = defaults.Symbols
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.FormValue("noupper") == "on" {
|
d.Digits, err = parseParam(r, "digits", defaults.Digits, maxPasswordLength)
|
||||||
d.NoUpper = true
|
validationErr = errors.Join(validationErr, err)
|
||||||
}
|
|
||||||
|
|
||||||
if r.FormValue("denyrepeat") == "on" {
|
d.Symbols, err = parseParam(r, "symbols", defaults.Symbols, maxPasswordLength)
|
||||||
d.DenyRepeat = true
|
validationErr = errors.Join(validationErr, err)
|
||||||
}
|
|
||||||
|
|
||||||
d.Password, err = generate(d.Length, d.Digits, d.Symbols, d.NoUpper, !d.DenyRepeat)
|
if validationErr != nil {
|
||||||
if err != nil {
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
d.Password = "Error: " + err.Error()
|
w.Write([]byte(validationErr.Error()))
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
w.Write([]byte(d.Password))
|
|
||||||
return
|
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.WriteHeader(http.StatusOK)
|
||||||
w.Write([]byte(d.Password))
|
w.Write([]byte(password))
|
||||||
default:
|
default:
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
w.Write([]byte("Sorry, only GET and POST methods are supported."))
|
w.Write([]byte("Sorry, only GET and POST methods are supported."))
|
||||||
|
|||||||
Reference in New Issue
Block a user