From 2c8a0b89676dbae36e3e877bd1ca251cbba373d9 Mon Sep 17 00:00:00 2001 From: engigu Date: Fri, 27 Mar 2026 16:47:12 +0800 Subject: [PATCH] chore: secure fix --- internal/executor/cron.go | 2 ++ internal/executor/executor.go | 1 + internal/models/task.go | 6 ++++++ internal/services/tasks/executor_service.go | 7 +++---- internal/services/tasks/ql_repo_parser.go | 16 +++++++++++----- internal/utils/shell.go | 10 ++++++++++ 6 files changed, 33 insertions(+), 9 deletions(-) diff --git a/internal/executor/cron.go b/internal/executor/cron.go index 8b33867..dec2f78 100644 --- a/internal/executor/cron.go +++ b/internal/executor/cron.go @@ -85,6 +85,7 @@ func (m *CronManager) AddTask(task CronTask) error { envs := task.GetEnvs() languages := task.GetLanguages() useMise := task.UseMise() + secrets := task.GetSecrets() schedule := strings.TrimSpace(task.GetSchedule()) entryID, err := m.cron.AddFunc(schedule, func() { @@ -109,6 +110,7 @@ func (m *CronManager) AddTask(task CronTask) error { } return ParseEnvVars(envs) }(), + Secrets: secrets, Languages: languages, UseMise: useMise, } diff --git a/internal/executor/executor.go b/internal/executor/executor.go index d75589d..9048a81 100644 --- a/internal/executor/executor.go +++ b/internal/executor/executor.go @@ -34,6 +34,7 @@ type CronTask interface { Task GetSchedule() string UseMise() bool + GetSecrets() []string GetRandomRange() int } diff --git a/internal/models/task.go b/internal/models/task.go index 3fab68f..b576840 100644 --- a/internal/models/task.go +++ b/internal/models/task.go @@ -90,6 +90,7 @@ type Task struct { Enabled bool `json:"enabled" gorm:"default:true"` RunningGo BigText `json:"running_go"` // 正在运行的 go routine id 数组 (JSON) RuntimeEnvs []string `json:"-" gorm:"-"` // 运行时环境变量(非持久化) + RuntimeSecrets []string `json:"-" gorm:"-"` // 运行时安全机密(非持久化) LastRun *LocalTime `json:"last_run"` NextRun *LocalTime `json:"next_run"` SourceID string `json:"source_id" gorm:"size:255;index"` // 脚本资源唯一标识(路径 sanitized) @@ -135,6 +136,10 @@ func (t *Task) GetEnvVars() []string { return t.RuntimeEnvs } +func (t *Task) GetSecrets() []string { + return t.RuntimeSecrets +} + func (t *Task) GetUseMise() bool { return t.AgentID == nil || *t.AgentID == "" } @@ -143,6 +148,7 @@ func (t *Task) UseMise() bool { return t.GetUseMise() } +// CronTask 计划任务接口 func (t *Task) GetSchedule() string { return t.Schedule } diff --git a/internal/services/tasks/executor_service.go b/internal/services/tasks/executor_service.go index fd8ba23..68f5741 100644 --- a/internal/services/tasks/executor_service.go +++ b/internal/services/tasks/executor_service.go @@ -510,7 +510,7 @@ func (es *ExecutorService) AddCronTask(task *models.Task) error { return nil } // 在加入调度器前,预先加载好环境信息 - task.RuntimeEnvs, _ = es.loadEnvVars(task.ID, string(task.Envs)) + task.RuntimeEnvs, task.RuntimeSecrets = es.loadEnvVars(task.ID, string(task.Envs)) return es.cronManager.AddTask(task) } @@ -997,11 +997,10 @@ func (es *ExecutorService) BuildRepoCommand(task *models.Task) (string, string) // 为了防止 shell 解释特殊字符(如 |),对每个参数进行转义/加引号 quotedArgs := make([]string, len(args)) for i, arg := range args { - // 使用单引号包裹参数,并转义已有的单引号 - quotedArgs[i] = "'" + strings.ReplaceAll(arg, "'", "'\\''") + "'" + quotedArgs[i] = utils.QuotePath(arg) } - cmdStr := "'" + strings.ReplaceAll(exePath, "'", "'\\''") + "' " + strings.Join(quotedArgs, " ") + cmdStr := utils.QuotePath(exePath) + " " + strings.Join(quotedArgs, " ") return buildRepoCommandEnvPrefix()+cmdStr, filepath.Dir(exePath) } diff --git a/internal/services/tasks/ql_repo_parser.go b/internal/services/tasks/ql_repo_parser.go index 3aacfcf..e8eb866 100644 --- a/internal/services/tasks/ql_repo_parser.go +++ b/internal/services/tasks/ql_repo_parser.go @@ -403,17 +403,18 @@ func isDir(path string) bool { } func getCommandByExt(ext, path string) string { + quotedPath := utils.QuotePath(path) switch ext { case ".js", ".ts": - return fmt.Sprintf("node %s", path) + return fmt.Sprintf("node %s", quotedPath) case ".py": - return fmt.Sprintf("python %s", path) + return fmt.Sprintf("python %s", quotedPath) case ".sh": - return fmt.Sprintf("bash %s", path) + return fmt.Sprintf("bash %s", quotedPath) case ".php": - return fmt.Sprintf("php %s", path) + return fmt.Sprintf("php %s", quotedPath) } - return path + return quotedPath } func matchesQLPattern(rel, filename string, keywordsStr string) bool { @@ -468,3 +469,8 @@ func splitKeywords(s string) []string { } return res } + +func resolveAbsScriptsDir() string { + cwd, _ := os.Getwd() + return filepath.Join(cwd, "data", "scripts") +} diff --git a/internal/utils/shell.go b/internal/utils/shell.go index 73324d2..5f7f1f6 100644 --- a/internal/utils/shell.go +++ b/internal/utils/shell.go @@ -69,3 +69,13 @@ func NewShellCommandCmd(command string) *exec.Cmd { shell, args := GetShellCommand(command) return exec.Command(shell, args...) } + +// QuotePath 转义并包裹路径,防止 Shell 注入 +func QuotePath(path string) string { + if path == "" { + return "''" + } + // 在 Unix-like 系统中,单引号包裹是最安全的 + // 需要将路径中的 ' 替换为 '\'' (结束当前引号,转义一个单引号,重新开启引号) + return "'" + strings.ReplaceAll(path, "'", "'\\''") + "'" +}