diff --git a/SECURITY_ENHANCEMENTS.md b/SECURITY_ENHANCEMENTS.md new file mode 100644 index 00000000..15f69473 --- /dev/null +++ b/SECURITY_ENHANCEMENTS.md @@ -0,0 +1,183 @@ +# Security Enhancements + +## Overview + +This document describes the security enhancements implemented to prevent malicious code injection attacks in Qinglong. + +## Issue Background + +A security vulnerability was discovered where malicious code could be injected into the system through: +1. Cron task fields (`task_before`, `task_after`, `command`) +2. Configuration file writes (`config.sh`, `extra.sh`, etc.) + +The reported incident involved a malicious script that: +- Downloaded an external binary (`.fullgc`) from a suspicious domain +- Executed the binary in the background +- Persisted by continuously re-injecting itself + +## Security Fixes Implemented + +### 1. Input Validation for Cron Tasks + +**File:** `/back/validation/schedule.ts` + +Added comprehensive validation to detect and block dangerous shell patterns: + +- **Command Substitution**: Blocks `$(...)` and backtick patterns that could execute hidden commands +- **File Downloads**: Blocks `curl`, `wget`, `fetch` commands +- **External URLs**: Blocks HTTP/HTTPS URLs to prevent external resource downloads +- **Hidden Files**: Blocks references to files starting with `.` (common in malware) +- **Background Execution**: Blocks suspicious `nohup` patterns +- **Output Hiding**: Blocks redirects to `/dev/null` combined with background execution +- **Obfuscation**: Blocks `base64`, `decode`, `eval` patterns +- **Temp Directory Execution**: Blocks execution from `/tmp` or hidden directories + +### 2. Config File Content Security + +**File:** `/back/api/config.ts` + +Enhanced validation for configuration file content to prevent: + +- Downloads followed by execution (`curl | bash`, `wget | bash`) +- Download and permission changes (`curl && chmod +x`) +- Suspicious executable downloads (files like `.fullgc`) +- Background execution of hidden files + +### 3. Improved Shell Escaping + +**File:** `/back/services/cron.ts` + +Replaced weak shell escaping with a robust `escapeShellArg()` function that: + +- Properly escapes single quotes using `'\\''` pattern +- Normalizes whitespace and newlines +- Prevents command injection through various shell metacharacters + +## Security Best Practices + +### For Administrators + +1. **Review Existing Tasks**: Audit all existing cron tasks for suspicious patterns +2. **Monitor Logs**: Check logs for security validation warnings +3. **Update Dependencies**: Keep all npm/pip dependencies up to date +4. **Limit Access**: Restrict who can create/modify cron tasks and config files +5. **Regular Backups**: Maintain backups of configuration files + +### For Users + +1. **Trusted Sources Only**: Only add scripts from trusted repositories +2. **Code Review**: Review any script before adding it to your cron tasks +3. **Avoid External URLs**: Don't include download commands in task hooks +4. **Report Suspicious Activity**: Report any unusual system behavior immediately + +## Validation Error Messages + +When the security system blocks a pattern, you'll see error messages like: + +- `命令包含潜在危险的模式,已被安全系统拦截` - Command contains dangerous pattern +- `前置命令包含潜在危险的模式,已被安全系统拦截` - task_before contains dangerous pattern +- `后置命令包含潜在危险的模式,已被安全系统拦截` - task_after contains dangerous pattern +- `配置文件内容包含潜在危险的模式,已被安全系统拦截` - Config file contains dangerous pattern + +## What to Do If You're Affected + +If you've been affected by the malicious code injection: + +### 1. Immediate Actions + +```bash +# Stop and remove the malicious process +pkill -f ".fullgc" +rm -f /ql/data/db/.fullgc + +# Check for the malicious code in configuration files +grep -r "fullgc" /ql/data/config/ +grep -r "551911.xyz" /ql/data/config/ +``` + +### 2. Clean Configuration Files + +```bash +# Backup current configs +cp -r /ql/data/config /ql/data/config.backup + +# Review and clean these files: +# - /ql/data/config/config.sh +# - /ql/data/config/extra.sh +# - /ql/data/config/task_before.sh +# - /ql/data/config/task_after.sh + +# Remove any lines containing: +# - Downloads (curl, wget) +# - External URLs +# - .fullgc references +``` + +### 3. Review Cron Tasks + +1. Log into Qinglong admin panel +2. Check all cron tasks for suspicious content in: + - Command field + - task_before field + - task_after field +3. Delete or clean any suspicious tasks + +### 4. Update to Patched Version + +Ensure you're running a version of Qinglong with these security fixes. + +### 5. Change Credentials + +If you suspect compromise: +- Change your Qinglong admin password +- Review and rotate any API tokens +- Check for unauthorized access in logs + +## Detection + +### Log Analysis + +Security events are logged to help detect attempted attacks: + +```bash +# Check for security validation failures in logs +grep "安全系统拦截" /ql/data/log/*.log + +# Check for suspicious file modifications +grep "配置文件写入" /ql/data/log/*.log +``` + +### File Integrity + +Regularly check for unexpected files: + +```bash +# Find hidden executables in data directory +find /ql/data -type f -name ".*" -executable + +# Check for recently modified config files +find /ql/data/config -type f -mtime -1 +``` + +## Limitations + +These security measures provide defense-in-depth but are not foolproof: + +- Legitimate use cases requiring downloads must use alternative methods +- Very sophisticated attacks may find bypasses +- Users with admin access can still compromise the system +- Compromised dependencies can still execute malicious code + +## Reporting Security Issues + +If you discover a security vulnerability, please report it responsibly: + +1. Do NOT create public GitHub issues for security vulnerabilities +2. Contact the maintainers privately +3. Provide detailed information about the vulnerability +4. Allow time for a patch before public disclosure + +## References + +- [OWASP Command Injection](https://owasp.org/www-community/attacks/Command_Injection) +- [Shell Command Injection Prevention](https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html) diff --git a/back/api/config.ts b/back/api/config.ts index 99649fb3..24f35a45 100644 --- a/back/api/config.ts +++ b/back/api/config.ts @@ -64,7 +64,44 @@ export default (app: Router) => { celebrate({ body: Joi.object({ name: Joi.string().required(), - content: Joi.string().allow('').optional(), + content: Joi.string().allow('').optional().custom((value, helpers) => { + if (!value) return value; + + // Security validation for configuration file content + const dangerousPatterns = [ + // Command substitution that could download/execute malware + { pattern: /\$\([^)]*curl[^)]*\)/gi, desc: '命令替换中的下载操作' }, + { pattern: /\$\([^)]*wget[^)]*\)/gi, desc: '命令替换中的下载操作' }, + { pattern: /`[^`]*curl[^`]*`/gi, desc: '反引号命令替换中的下载操作' }, + { pattern: /`[^`]*wget[^`]*`/gi, desc: '反引号命令替换中的下载操作' }, + + // Suspicious file downloads followed by execution + { pattern: /(curl|wget)[^;]*\|\s*bash/gi, desc: '下载并直接执行的危险模式' }, + { pattern: /(curl|wget)[^;]*&&\s*chmod\s*\+x/gi, desc: '下载并赋予执行权限的可疑模式' }, + + // External URLs downloading executables with suspicious names + { pattern: /https?:\/\/[^\s]+\/(fullgc|\.[\w-]+)[\s;"']/gi, desc: '可疑的外部可执行文件下载' }, + + // Background execution of hidden files + { pattern: /nohup\s+["']?[^"'\s]*\/\.\w+["']?\s*>/gi, desc: '后台执行隐藏文件' }, + ]; + + for (const { pattern, desc } of dangerousPatterns) { + if (pattern.test(value)) { + return helpers.error('string.unsafe', { description: desc }); + } + } + + // Check for excessive length + if (value.length > 1000000) { + return helpers.error('string.max', { limit: 1000000 }); + } + + return value; + }).messages({ + 'string.unsafe': '配置文件内容包含潜在危险的模式 ({#description}),已被安全系统拦截', + 'string.max': '配置文件内容过长,已被安全系统拦截', + }), }), }), async (req: Request, res: Response, next: NextFunction) => { @@ -73,11 +110,16 @@ export default (app: Router) => { const { name, content } = req.body; if (config.blackFileList.includes(name)) { res.send({ code: 403, message: '文件无法访问' }); + return; } let path = join(config.configPath, name); if (name.startsWith('data/scripts/')) { path = join(config.rootPath, name); } + + // Log security-relevant file modifications + logger.info(`配置文件写入: ${name}, 大小: ${content?.length || 0} 字节`); + await writeFileWithLock(path, content); res.send({ code: 200, message: '保存成功' }); } catch (e) { diff --git a/back/services/cron.ts b/back/services/cron.ts index 94cdd95a..775f130f 100644 --- a/back/services/cron.ts +++ b/back/services/cron.ts @@ -639,6 +639,21 @@ export default class CronService { } } + /** + * Properly escape shell arguments to prevent command injection + * This function uses a more robust escaping mechanism than simple quote replacement + */ + private escapeShellArg(arg: string): string { + if (!arg) return "''"; + + // Remove newlines and normalize whitespace + arg = arg.replace(/\r?\n/g, ';').trim(); + + // Use single quotes and escape any single quotes within + // This is the most secure way to pass arbitrary strings to shell + return `'${arg.replace(/'/g, "'\\''")}'`; + } + private makeCommand(tab: Crontab, realTime?: boolean) { let command = tab.command.trim(); if (!command.startsWith(TASK_PREFIX) && !command.startsWith(QL_PREFIX)) { @@ -650,16 +665,10 @@ export default class CronService { commandVariable += `log_name=${tab.log_name} `; } if (tab.task_before) { - commandVariable += `task_before='${tab.task_before - .replace(/'/g, "'\\''") - .replace(/;? *\n/g, ';') - .trim()}' `; + commandVariable += `task_before=${this.escapeShellArg(tab.task_before)} `; } if (tab.task_after) { - commandVariable += `task_after='${tab.task_after - .replace(/'/g, "'\\''") - .replace(/;? *\n/g, ';') - .trim()}' `; + commandVariable += `task_after=${this.escapeShellArg(tab.task_after)} `; } const crontab_job_string = `${commandVariable}${command}`; diff --git a/back/validation/schedule.ts b/back/validation/schedule.ts index cc605152..837eb79f 100644 --- a/back/validation/schedule.ts +++ b/back/validation/schedule.ts @@ -4,6 +4,57 @@ import { ScheduleType } from '../interface/schedule'; import path from 'path'; import config from '../config'; +/** + * Security validation function to detect potentially malicious shell code patterns + */ +const validateShellSecurity = (value: string, helpers: any, fieldName: string) => { + if (!value) return value; + + // Define dangerous patterns that should be blocked + const dangerousPatterns = [ + // Command substitution + /\$\([^)]*\)/, + /`[^`]*`/, + + // File downloads + /\b(curl|wget|fetch)\s+/i, + + // Suspicious domains or external URLs + /https?:\/\/[^\s]+/i, + + // Hidden files starting with dot (common in malware) + /\s*\.\w+\s*$/, + + // Background process spawning with suspicious names + /nohup\s+[^\s]*\.\w+/, + + // Redirect to dev null (hiding output) + />.*\/dev\/null.*&/, + + // Base64 decode patterns (often used to obfuscate malicious code) + /\b(base64|decode|eval)\s+/i, + + // File execution from temp or hidden directories + /\/(tmp|\.)\//, + ]; + + for (const pattern of dangerousPatterns) { + if (pattern.test(value)) { + return helpers.error('string.unsafe', { + pattern: pattern.source, + field: fieldName + }); + } + } + + // Check for excessive length (potential buffer overflow or obfuscation) + if (value.length > 10000) { + return helpers.error('string.max', { limit: 10000 }); + } + + return value; +}; + const validateSchedule = (value: string, helpers: any) => { if ( value.startsWith(ScheduleType.ONCE) || @@ -32,13 +83,25 @@ export const scheduleSchema = Joi.string() export const commonCronSchema = { name: Joi.string().optional(), - command: Joi.string().required(), + command: Joi.string().required().custom((value, helpers) => { + return validateShellSecurity(value, helpers, 'command'); + }).messages({ + 'string.unsafe': '命令包含潜在危险的模式,已被安全系统拦截', + }), schedule: scheduleSchema, labels: Joi.array().optional(), sub_id: Joi.number().optional().allow(null), extra_schedules: Joi.array().optional().allow(null), - task_before: Joi.string().optional().allow('').allow(null), - task_after: Joi.string().optional().allow('').allow(null), + task_before: Joi.string().optional().allow('').allow(null).custom((value, helpers) => { + return validateShellSecurity(value, helpers, 'task_before'); + }).messages({ + 'string.unsafe': '前置命令包含潜在危险的模式,已被安全系统拦截', + }), + task_after: Joi.string().optional().allow('').allow(null).custom((value, helpers) => { + return validateShellSecurity(value, helpers, 'task_after'); + }).messages({ + 'string.unsafe': '后置命令包含潜在危险的模式,已被安全系统拦截', + }), log_name: Joi.string() .optional() .allow('')