Add server-side app name validation (closes #37) #49
@ -41,7 +41,7 @@ func (h *Handlers) HandleAppNew() http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// HandleAppCreate handles app creation.
|
// HandleAppCreate handles app creation.
|
||||||
func (h *Handlers) HandleAppCreate() http.HandlerFunc {
|
func (h *Handlers) HandleAppCreate() http.HandlerFunc { //nolint:funlen // validation adds necessary length
|
||||||
tmpl := templates.GetParsed()
|
tmpl := templates.GetParsed()
|
||||||
|
|
||||||
return func(writer http.ResponseWriter, request *http.Request) {
|
return func(writer http.ResponseWriter, request *http.Request) {
|
||||||
@ -71,6 +71,14 @@ func (h *Handlers) HandleAppCreate() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nameErr := validateAppName(name)
|
||||||
|
if nameErr != nil {
|
||||||
|
data["Error"] = "Invalid app name: " + nameErr.Error()
|
||||||
|
_ = tmpl.ExecuteTemplate(writer, "app_new.html", data)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if branch == "" {
|
if branch == "" {
|
||||||
branch = "main"
|
branch = "main"
|
||||||
}
|
}
|
||||||
@ -194,7 +202,7 @@ func (h *Handlers) HandleAppEdit() http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// HandleAppUpdate handles app updates.
|
// HandleAppUpdate handles app updates.
|
||||||
func (h *Handlers) HandleAppUpdate() http.HandlerFunc {
|
func (h *Handlers) HandleAppUpdate() http.HandlerFunc { //nolint:funlen // validation adds necessary length
|
||||||
tmpl := templates.GetParsed()
|
tmpl := templates.GetParsed()
|
||||||
|
|
||||||
return func(writer http.ResponseWriter, request *http.Request) {
|
return func(writer http.ResponseWriter, request *http.Request) {
|
||||||
@ -214,7 +222,20 @@ func (h *Handlers) HandleAppUpdate() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
application.Name = request.FormValue("name")
|
newName := request.FormValue("name")
|
||||||
|
|
||||||
|
nameErr := validateAppName(newName)
|
||||||
|
if nameErr != nil {
|
||||||
|
data := h.addGlobals(map[string]any{
|
||||||
|
"App": application,
|
||||||
|
"Error": "Invalid app name: " + nameErr.Error(),
|
||||||
|
}, request)
|
||||||
|
_ = tmpl.ExecuteTemplate(writer, "app_edit.html", data)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
application.Name = newName
|
||||||
application.RepoURL = request.FormValue("repo_url")
|
application.RepoURL = request.FormValue("repo_url")
|
||||||
application.Branch = request.FormValue("branch")
|
application.Branch = request.FormValue("branch")
|
||||||
application.DockerfilePath = request.FormValue("dockerfile_path")
|
application.DockerfilePath = request.FormValue("dockerfile_path")
|
||||||
|
|||||||
44
internal/handlers/app_name_validation.go
Normal file
44
internal/handlers/app_name_validation.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// appNameMinLength is the minimum allowed length for an app name.
|
||||||
|
appNameMinLength = 2
|
||||||
|
// appNameMaxLength is the maximum allowed length for an app name.
|
||||||
|
appNameMaxLength = 63
|
||||||
|
)
|
||||||
|
|
||||||
|
// validAppNameRe matches names containing only lowercase alphanumeric characters and
|
||||||
|
// hyphens, starting and ending with an alphanumeric character.
|
||||||
|
var validAppNameRe = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*[a-z0-9]$`)
|
||||||
|
|
||||||
|
// validateAppName checks that the given app name is safe for use in Docker
|
||||||
|
// container names, image tags, and file system paths.
|
||||||
|
var (
|
||||||
|
errAppNameLength = errors.New(
|
||||||
|
"app name must be between " +
|
||||||
|
strconv.Itoa(appNameMinLength) + " and " +
|
||||||
|
strconv.Itoa(appNameMaxLength) + " characters",
|
||||||
|
)
|
||||||
|
errAppNamePattern = errors.New(
|
||||||
|
"app name must contain only lowercase letters, numbers, " +
|
||||||
|
"and hyphens, and must start and end with a letter or number",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
func validateAppName(name string) error {
|
||||||
|
if len(name) < appNameMinLength || len(name) > appNameMaxLength {
|
||||||
|
return errAppNameLength
|
||||||
|
}
|
||||||
|
|
||||||
|
if !validAppNameRe.MatchString(name) {
|
||||||
|
return errAppNamePattern
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
48
internal/handlers/app_name_validation_test.go
Normal file
48
internal/handlers/app_name_validation_test.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package handlers //nolint:testpackage // testing unexported validateAppName
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateAppName(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"valid simple", "myapp", false},
|
||||||
|
{"valid with hyphen", "my-app", false},
|
||||||
|
{"valid with numbers", "app123", false},
|
||||||
|
{"valid two chars", "ab", false},
|
||||||
|
{"valid complex", "my-cool-app-v2", false},
|
||||||
|
{"valid all numbers", "123", false},
|
||||||
|
{"empty", "", true},
|
||||||
|
{"single char", "a", true},
|
||||||
|
{"too long", "a" + string(make([]byte, 63)), true},
|
||||||
|
{"exactly 63 chars", "a23456789012345678901234567890123456789012345678901234567890123", false},
|
||||||
|
{"64 chars", "a234567890123456789012345678901234567890123456789012345678901234", true},
|
||||||
|
{"uppercase", "MyApp", true},
|
||||||
|
{"spaces", "my app", true},
|
||||||
|
{"starts with hyphen", "-myapp", true},
|
||||||
|
{"ends with hyphen", "myapp-", true},
|
||||||
|
{"underscore", "my_app", true},
|
||||||
|
{"dot", "my.app", true},
|
||||||
|
{"slash", "my/app", true},
|
||||||
|
{"path traversal", "../etc/passwd", true},
|
||||||
|
{"special chars", "app@name!", true},
|
||||||
|
{"unicode", "appñame", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
err := validateAppName(tt.input)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("validateAppName(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user