diff --git a/back/services/cron.ts b/back/services/cron.ts index a6ffea23..8f598b34 100644 --- a/back/services/cron.ts +++ b/back/services/cron.ts @@ -477,19 +477,33 @@ export default class CronService { ); let { id, command, log_path, log_name } = cron; - // Sanitize log_name to prevent path traversal - const sanitizedLogName = log_name - ? log_name.replace(/[\/\\\.]/g, '_').replace(/^_+|_+$/g, '') - : ''; - const uniqPath = - sanitizedLogName || (await getUniqPath(command, `${id}`)); - const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS'); - const logDirPath = path.resolve(config.logPath, `${uniqPath}`); - if (log_path?.split('/')?.every((x) => x !== uniqPath)) { - await fs.mkdir(logDirPath, { recursive: true }); + + // Check if log_name is an absolute path (e.g., /dev/null) + const isAbsolutePath = log_name && log_name.startsWith('/'); + + let uniqPath: string; + let absolutePath: string; + let logPath: string; + + if (isAbsolutePath) { + // Use absolute path directly for special files like /dev/null + uniqPath = log_name!; + absolutePath = log_name!; + logPath = log_name!; + } else { + // Sanitize log_name to prevent path traversal for relative paths + const sanitizedLogName = log_name + ? log_name.replace(/[\/\\\.]/g, '_').replace(/^_+|_+$/g, '') + : ''; + uniqPath = sanitizedLogName || (await getUniqPath(command, `${id}`)); + const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS'); + const logDirPath = path.resolve(config.logPath, `${uniqPath}`); + if (log_path?.split('/')?.every((x) => x !== uniqPath)) { + await fs.mkdir(logDirPath, { recursive: true }); + } + logPath = `${uniqPath}/${logTime}.log`; + absolutePath = path.resolve(config.logPath, `${logPath}`); } - const logPath = `${uniqPath}/${logTime}.log`; - const absolutePath = path.resolve(config.logPath, `${logPath}`); const cp = spawn( `real_log_path=${logPath} no_delay=true ${this.makeCommand( cron, diff --git a/back/validation/schedule.ts b/back/validation/schedule.ts index 56e15f84..8d0a4f54 100644 --- a/back/validation/schedule.ts +++ b/back/validation/schedule.ts @@ -41,8 +41,21 @@ export const commonCronSchema = { .optional() .allow('') .allow(null) - .pattern(/^[a-zA-Z0-9_-]+$/) - .max(100) + .custom((value, helpers) => { + if (!value) return value; + // Allow absolute paths like /dev/null + if (value.startsWith('/')) { + return value; + } + // For relative names, enforce strict pattern + if (!/^[a-zA-Z0-9_-]+$/.test(value)) { + return helpers.error('string.pattern.base'); + } + if (value.length > 100) { + return helpers.error('string.max'); + } + return value; + }) .messages({ 'string.pattern.base': '日志名称只能包含字母、数字、下划线和连字符', 'string.max': '日志名称不能超过100个字符', diff --git a/src/locales/en-US.json b/src/locales/en-US.json index 1acaf8ad..3412934e 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -524,7 +524,9 @@ "清除成功": "Clean successful", "日志名称": "Log Name", "自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成": "Custom log folder name to distinguish logs from different tasks. Leave blank to auto-generate", + "自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成。支持绝对路径如 /dev/null": "Custom log folder name to distinguish logs from different tasks. Leave blank to auto-generate. Supports absolute paths like /dev/null", "请输入自定义日志文件夹名称": "Please enter a custom log folder name", + "请输入自定义日志文件夹名称或绝对路径": "Please enter a custom log folder name or absolute path", "日志名称只能包含字母、数字、下划线和连字符": "Log name can only contain letters, numbers, underscores and hyphens", "日志名称不能超过100个字符": "Log name cannot exceed 100 characters" } diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index 75d40598..0a7d81b9 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -524,7 +524,9 @@ "清除成功": "清除成功", "日志名称": "日志名称", "自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成": "自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成", + "自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成。支持绝对路径如 /dev/null": "自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成。支持绝对路径如 /dev/null", "请输入自定义日志文件夹名称": "请输入自定义日志文件夹名称", + "请输入自定义日志文件夹名称或绝对路径": "请输入自定义日志文件夹名称或绝对路径", "日志名称只能包含字母、数字、下划线和连字符": "日志名称只能包含字母、数字、下划线和连字符", "日志名称不能超过100个字符": "日志名称不能超过100个字符" } diff --git a/src/pages/crontab/modal.tsx b/src/pages/crontab/modal.tsx index ae3552e3..79fb781d 100644 --- a/src/pages/crontab/modal.tsx +++ b/src/pages/crontab/modal.tsx @@ -184,22 +184,31 @@ const CronModal = ({ name="log_name" label={intl.get('日志名称')} tooltip={intl.get( - '自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成', + '自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成。支持绝对路径如 /dev/null', )} rules={[ { - pattern: /^[a-zA-Z0-9_-]*$/, - message: intl.get('日志名称只能包含字母、数字、下划线和连字符'), - }, - { - max: 100, - message: intl.get('日志名称不能超过100个字符'), + validator: (_, value) => { + if (!value) return Promise.resolve(); + // Allow absolute paths + if (value.startsWith('/')) return Promise.resolve(); + // For relative names, enforce strict pattern + if (!/^[a-zA-Z0-9_-]+$/.test(value)) { + return Promise.reject( + intl.get('日志名称只能包含字母、数字、下划线和连字符'), + ); + } + if (value.length > 100) { + return Promise.reject(intl.get('日志名称不能超过100个字符')); + } + return Promise.resolve(); + }, }, ]} >