fix: restrict app names to prevent command injection

The previous app name validation regex permitted shell metacharacters such as `;`, `$`, backticks, `|`, and `&`. These names were embedded unquoted into the generated git pre-receive hook script, allowing an authenticated user to execute arbitrary commands as the dokku user simply by pushing to a remote with a crafted app name. App names are now restricted to lowercase alphanumerics, dots, and hyphens, and the hook script also quotes the app variable as a defense-in-depth measure.
This commit is contained in:
Jose Diaz-Gonzalez
2026-05-09 12:46:02 -04:00
parent d00aaf3ff3
commit 93f87a0f07
5 changed files with 85 additions and 15 deletions

View File

@@ -923,12 +923,12 @@ func IsValidAppName(appName string) error {
return errors.New("Please specify an app to run the command on")
}
r, _ := regexp.Compile("^[a-z0-9][^/:_A-Z]*$")
r, _ := regexp.Compile("^[a-z0-9][a-z0-9.-]*$")
if r.MatchString(appName) {
return nil
}
return errors.New("App name must begin with lowercase alphanumeric character, and cannot include uppercase characters, colons, or underscores")
return errors.New("App name must begin with lowercase alphanumeric character, and may only contain lowercase alphanumerics, dots, and hyphens")
}
// isValidAppNameOld verifies that the app name matches the old naming restrictions
@@ -937,12 +937,12 @@ func isValidAppNameOld(appName string) error {
return errors.New("Please specify an app to run the command on")
}
r, _ := regexp.Compile("^[a-z0-9][^/:A-Z]*$")
r, _ := regexp.Compile("^[a-z0-9][a-z0-9.-]*$")
if r.MatchString(appName) {
return nil
}
return errors.New("App name must begin with lowercase alphanumeric character, and cannot include uppercase characters, or colons")
return errors.New("App name must begin with lowercase alphanumeric character, and may only contain lowercase alphanumerics, dots, and hyphens")
}
// AppDoesNotExist wraps error to include the app name

View File

@@ -77,6 +77,60 @@ func TestCommonVerifyAppNameInvalid(t *testing.T) {
Expect(VerifyAppName("1994testApp")).To(HaveOccurred())
}
func TestCommonIsValidAppName(t *testing.T) {
RegisterTestingT(t)
validNames := []string{
"myapp",
"my-app",
"my.app",
"app123",
"123app",
"my-app.prod",
"a",
"0",
}
for _, name := range validNames {
Expect(IsValidAppName(name)).To(Succeed(), "expected %q to be a valid app name", name)
}
}
func TestCommonIsValidAppNameRejectsShellMetacharacters(t *testing.T) {
RegisterTestingT(t)
invalidNames := []string{
"app;id",
"app$cmd",
"app`whoami`",
"app|cat",
"app&background",
"app>file",
"app<file",
"app(cmd)",
"app{cmd}",
"app[cmd]",
"app*",
"app?",
"app!",
"app#",
"app\\cmd",
"app\"quote",
"app'quote",
"app cmd",
"app\tcmd",
"app\ncmd",
"app~cmd",
"app/cmd",
"app:cmd",
"app_name",
"App",
"-app",
".app",
"",
}
for _, name := range invalidNames {
Expect(IsValidAppName(name)).To(HaveOccurred(), "expected %q to be rejected", name)
}
}
func TestCommonVerifyAppName(t *testing.T) {
RegisterTestingT(t)
Expect(setupTests()).To(Succeed())

View File

@@ -222,10 +222,8 @@ fn-is-valid-app-name() {
declare desc="verify that the app name matches naming restrictions"
local APP="$1"
[[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on"
if [[ "$APP" =~ ^[a-z].* ]] || [[ "$APP" =~ ^[0-9].* ]]; then
if [[ ! $APP =~ [A-Z] ]] && [[ ! $APP =~ [:] ]] && [[ ! $APP =~ [_] ]]; then
return 0
fi
if [[ "$APP" =~ ^[a-z0-9][a-z0-9.-]*$ ]]; then
return 0
fi
return 1
@@ -235,10 +233,8 @@ fn-is-valid-app-name-old() {
declare desc="verify that the app name matches the old naming restrictions"
local APP="$1"
[[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on"
if [[ "$APP" =~ ^[a-z].* ]] || [[ "$APP" =~ ^[0-9].* ]]; then
if [[ ! $APP =~ [A-Z] ]] && [[ ! $APP =~ [:] ]]; then
return 0
fi
if [[ "$APP" =~ ^[a-z0-9][a-z0-9.-]*$ ]]; then
return 0
fi
return 1
@@ -265,7 +261,7 @@ is_valid_app_name() {
return
fi
dokku_log_fail "App name must begin with lowercase alphanumeric character, and cannot include uppercase characters, colons, or underscores"
dokku_log_fail "App name must begin with lowercase alphanumeric character, and may only contain lowercase alphanumerics, dots, and hyphens"
}
is_valid_app_name_old() {
@@ -276,7 +272,7 @@ is_valid_app_name_old() {
return
fi
dokku_log_fail "App name must begin with lowercase alphanumeric character, and cannot include uppercase characters, or colons"
dokku_log_fail "App name must begin with lowercase alphanumeric character, and may only contain lowercase alphanumerics, dots, and hyphens"
}
verify_app_name() {

View File

@@ -464,7 +464,7 @@ fn-git-create-hook() {
set -e
set -o pipefail
cat | DOKKU_ROOT="$DOKKU_ROOT" dokku git-hook $APP
cat | DOKKU_ROOT="$DOKKU_ROOT" dokku git-hook "$APP"
EOF
chmod +x "$PRERECEIVE_HOOK"
}

View File

@@ -96,6 +96,26 @@ teardown() {
echo "status: $status"
assert_failure
run /bin/bash -c "dokku apps:create 'test;id'"
echo "output: $output"
echo "status: $status"
assert_failure
run /bin/bash -c "dokku apps:create 'test\$cmd'"
echo "output: $output"
echo "status: $status"
assert_failure
run /bin/bash -c "dokku apps:create 'test|cmd'"
echo "output: $output"
echo "status: $status"
assert_failure
run /bin/bash -c "dokku apps:create 'test&cmd'"
echo "output: $output"
echo "status: $status"
assert_failure
run /bin/bash -c "dokku --app $TEST_APP apps:create"
echo "output: $output"
echo "status: $status"