Files
dokku/plugins/cron/cron_test.go
Jose Diaz-Gonzalez ac95225809 fix: prevent host shell injection from app.json cron commands
The docker-local scheduler wrote each app.json cron command verbatim into the dokku user's crontab, where cron's `bash -c` interpreted any shell metacharacters in the command on the host as the dokku user rather than inside the container. The crontab line is now `dokku cron:run <app> <cron-id>`, and the command is resolved from app.json and exec'd inside the container at run time. Commands containing shell operators are also rejected when app.json is validated at deploy time.
2026-05-25 02:14:21 -04:00

103 lines
2.5 KiB
Go

package cron
import (
"strings"
"testing"
)
func TestDokkuRunCommandAppTaskDispatchesViaCronRun(t *testing.T) {
task := CronTask{
App: "myapp",
ID: "abc123",
Command: "echo CRON_OK; echo hi > /tmp/appjson-test.txt",
Schedule: "* * * * *",
ConcurrencyPolicy: "allow",
}
got := task.DokkuRunCommand()
want := "dokku cron:run myapp abc123"
if got != want {
t.Errorf("DokkuRunCommand() = %q, want %q", got, want)
}
if strings.Contains(got, task.Command) {
t.Errorf("DokkuRunCommand() leaked user command into crontab line: %q", got)
}
if strings.ContainsAny(got, ";>|&`$") {
t.Errorf("DokkuRunCommand() contains shell metacharacters: %q", got)
}
}
func TestDokkuRunCommandPlainCommandStillUsesCronRun(t *testing.T) {
task := CronTask{
App: "myapp",
ID: "abc123",
Command: "npm run send-email",
Schedule: "@daily",
ConcurrencyPolicy: "forbid",
}
got := task.DokkuRunCommand()
want := "dokku cron:run myapp abc123"
if got != want {
t.Errorf("DokkuRunCommand() = %q, want %q", got, want)
}
}
func TestDokkuRunCommandAltCommandUnchanged(t *testing.T) {
task := CronTask{
ID: "abc123",
AltCommand: "/usr/bin/some-internal-task --flag",
}
got := task.DokkuRunCommand()
want := "/usr/bin/some-internal-task --flag"
if got != want {
t.Errorf("DokkuRunCommand() = %q, want %q", got, want)
}
}
func TestValidateCronCommandAcceptsValidCommands(t *testing.T) {
cases := []string{
"python3 task.py schedule",
"npm run send-email",
"sh -c 'echo CRON_OK; echo hi > /tmp/x.txt'",
`node -e 'console.log(1)'`,
"true",
}
for _, cmd := range cases {
if err := ValidateCronCommand(cmd); err != nil {
t.Errorf("ValidateCronCommand(%q) returned error: %v", cmd, err)
}
}
}
func TestValidateCronCommandRejectsShellOperators(t *testing.T) {
cases := []string{
"echo CRON_OK; echo hi > /tmp/x.txt",
"cmd1 && cmd2",
"cmd | other",
"cmd > file",
"cmd $(other)",
}
for _, cmd := range cases {
if err := ValidateCronCommand(cmd); err == nil {
t.Errorf("ValidateCronCommand(%q) accepted a command containing a shell operator", cmd)
}
}
}
func TestDokkuRunCommandAltCommandWithLogFile(t *testing.T) {
task := CronTask{
ID: "abc123",
AltCommand: "/usr/bin/some-internal-task",
LogFile: "/var/log/dokku/internal-task.log",
}
got := task.DokkuRunCommand()
want := "/usr/bin/some-internal-task &>> /var/log/dokku/internal-task.log"
if got != want {
t.Errorf("DokkuRunCommand() = %q, want %q", got, want)
}
}