增加 sudo 命令判断

This commit is contained in:
whyour 2026-06-13 00:29:32 +08:00
parent 7d8feadc78
commit 96b4c90398
10 changed files with 62 additions and 24 deletions

View File

@ -12,6 +12,11 @@ import multer from 'multer';
import { writeFileWithLock } from '../shared/utils'; import { writeFileWithLock } from '../shared/utils';
const route = Router(); const route = Router();
function isPathAllowed(targetPath: string): boolean {
const resolved = path.resolve(targetPath);
return config.writePathList.some((x) => resolved.startsWith(x));
}
const storage = multer.diskStorage({ const storage = multer.diskStorage({
destination: function (req, file, cb) { destination: function (req, file, cb) {
cb(null, config.scriptPath); cb(null, config.scriptPath);
@ -161,24 +166,32 @@ export default (app: Router) => {
} }
if (req.file) { if (req.file) {
await fs.rename(req.file.path, join(path, filename)); const uploadPath = join(path, filename);
if (!isPathAllowed(uploadPath)) {
return res.send({ code: 403, message: t('暂无权限') });
}
await fs.rename(req.file.path, uploadPath);
return res.send({ code: 200 }); return res.send({ code: 200 });
} }
if (directory) { if (directory) {
await fs.mkdir(join(path, directory), { recursive: true }); const dirPath = join(path, directory);
if (!isPathAllowed(dirPath)) {
return res.send({ code: 403, message: t('暂无权限') });
}
await fs.mkdir(dirPath, { recursive: true });
return res.send({ code: 200 }); return res.send({ code: 200 });
} }
if (!originFilename) { if (!originFilename) {
originFilename = filename; originFilename = filename;
} }
const originFilePath = join( const originFilePath = join(path, originFilename);
path, const filePath = join(path, filename);
`${originFilename.replace(/\//g, '')}`, if (!isPathAllowed(filePath) || !isPathAllowed(originFilePath)) {
); return res.send({ code: 403, message: t('暂无权限') });
}
await fs.mkdir(path, { recursive: true }); await fs.mkdir(path, { recursive: true });
const filePath = join(path, `${filename.replace(/\//g, '')}`);
const fileExists = await fileExist(filePath); const fileExists = await fileExist(filePath);
if (fileExists) { if (fileExists) {
await fs.copyFile( await fs.copyFile(
@ -317,6 +330,9 @@ export default (app: Router) => {
} }
const { name, ext } = parse(filename); const { name, ext } = parse(filename);
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`); const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
if (!isPathAllowed(filePath)) {
return res.send({ code: 403, message: t('暂无权限') });
}
await writeFileWithLock(filePath, content || ''); await writeFileWithLock(filePath, content || '');
const scriptService = Container.get(ScriptService); const scriptService = Container.get(ScriptService);
@ -345,6 +361,9 @@ export default (app: Router) => {
} }
const { name, ext } = parse(filename); const { name, ext } = parse(filename);
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`); const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
if (!isPathAllowed(filePath)) {
return res.send({ code: 403, message: t('暂无权限') });
}
const logPath = join(config.logPath, path, `${name}.swap`); const logPath = join(config.logPath, path, `${name}.swap`);
const scriptService = Container.get(ScriptService); const scriptService = Container.get(ScriptService);
@ -380,6 +399,9 @@ export default (app: Router) => {
} }
const filePath = join(config.scriptPath, path, filename); const filePath = join(config.scriptPath, path, filename);
const newPath = join(config.scriptPath, path, newFilename); const newPath = join(config.scriptPath, path, newFilename);
if (!isPathAllowed(filePath) || !isPathAllowed(newPath)) {
return res.send({ code: 403, message: t('暂无权限') });
}
await fs.rename(filePath, newPath); await fs.rename(filePath, newPath);
res.send({ code: 200 }); res.send({ code: 200 });
} catch (e) { } catch (e) {

View File

@ -1,3 +1,5 @@
import { maybeSudo } from './container';
export const LOG_END_SYMBOL = '     '; export const LOG_END_SYMBOL = '     ';
export const TASK_COMMAND = 'task'; export const TASK_COMMAND = 'task';
@ -60,17 +62,17 @@ export const LINUX_DEPENDENCE_COMMAND: Record<
} }
> = { > = {
Debian: { Debian: {
install: 'sudo apt-get install -y', install: maybeSudo('apt-get install -y'),
uninstall: 'sudo apt-get remove -y', uninstall: maybeSudo('apt-get remove -y'),
info: 'sudo dpkg-query -s', info: maybeSudo('dpkg-query -s'),
check(info: string) { check(info: string) {
return info.includes('install ok installed'); return info.includes('install ok installed');
}, },
}, },
Ubuntu: { Ubuntu: {
install: 'sudo apt-get install -y', install: maybeSudo('apt-get install -y'),
uninstall: 'sudo apt-get remove -y', uninstall: maybeSudo('apt-get remove -y'),
info: 'sudo dpkg-query -s', info: maybeSudo('dpkg-query -s'),
check(info: string) { check(info: string) {
return info.includes('install ok installed'); return info.includes('install ok installed');
}, },

7
back/config/container.ts Normal file
View File

@ -0,0 +1,7 @@
export function isInContainer(): boolean {
return process.env.QL_CONTAINER === 'true';
}
export function maybeSudo(cmd: string): string {
return isInContainer() ? `sudo ${cmd}` : cmd;
}

View File

@ -11,6 +11,7 @@ import { writeFileWithLock } from '../shared/utils';
import { DependenceTypes } from '../data/dependence'; import { DependenceTypes } from '../data/dependence';
import { FormData } from 'undici'; import { FormData } from 'undici';
import os from 'os'; import os from 'os';
import { maybeSudo, isInContainer } from './container';
export * from './share'; export * from './share';
@ -557,8 +558,8 @@ export async function setSystemTimezone(timezone: string): Promise<boolean> {
throw new Error('Invalid timezone'); throw new Error('Invalid timezone');
} }
await promiseExec(`sudo ln -sf /usr/share/zoneinfo/${timezone} /etc/localtime`); await promiseExec(maybeSudo(`ln -sf /usr/share/zoneinfo/${timezone} /etc/localtime`));
await promiseExec(`echo "${timezone}" | sudo tee /etc/timezone`); await promiseExec(`echo "${timezone}" | ${maybeSudo('tee /etc/timezone')}`);
return true; return true;
} catch (error) { } catch (error) {
@ -584,7 +585,7 @@ except:
''')"`, ''')"`,
[DependenceTypes.linux]: getOsTypeSync() === 'Alpine' [DependenceTypes.linux]: getOsTypeSync() === 'Alpine'
? `apk info -es ${name}` ? `apk info -es ${name}`
: `sudo dpkg-query -s ${name}`, : maybeSudo(`dpkg-query -s ${name}`),
}; };
return baseCommands[type]; return baseCommands[type];
@ -597,7 +598,7 @@ export function getInstallCommand(type: DependenceTypes, name: string): string {
'pip3 install --disable-pip-version-check --root-user-action=ignore', 'pip3 install --disable-pip-version-check --root-user-action=ignore',
[DependenceTypes.linux]: getOsTypeSync() === 'Alpine' [DependenceTypes.linux]: getOsTypeSync() === 'Alpine'
? 'apk add --no-check-certificate' ? 'apk add --no-check-certificate'
: 'sudo apt-get install -y', : maybeSudo('apt-get install -y'),
}; };
let command = baseCommands[type]; let command = baseCommands[type];
@ -619,7 +620,7 @@ export function getUninstallCommand(
'pip3 uninstall --disable-pip-version-check --root-user-action=ignore -y', 'pip3 uninstall --disable-pip-version-check --root-user-action=ignore -y',
[DependenceTypes.linux]: getOsTypeSync() === 'Alpine' [DependenceTypes.linux]: getOsTypeSync() === 'Alpine'
? 'apk del' ? 'apk del'
: 'sudo apt-get remove -y', : maybeSudo('apt-get remove -y'),
}; };
return `${baseCommands[type]} ${name.trim()}`; return `${baseCommands[type]} ${name.trim()}`;
@ -732,23 +733,24 @@ async function _updateLinuxMirror(
osType: string, osType: string,
mirrorDomainWithScheme: string, mirrorDomainWithScheme: string,
): Promise<string> { ): Promise<string> {
const S = isInContainer() ? 'sudo ' : '';
let filePath: string, currentDomainWithScheme: string | null; let filePath: string, currentDomainWithScheme: string | null;
switch (osType) { switch (osType) {
case 'Debian': case 'Debian':
filePath = '/etc/apt/sources.list.d/debian.sources'; filePath = '/etc/apt/sources.list.d/debian.sources';
currentDomainWithScheme = await getCurrentMirrorDomain(filePath); currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
if (currentDomainWithScheme) { if (currentDomainWithScheme) {
return `sudo sed -i 's|${currentDomainWithScheme}|${mirrorDomainWithScheme || 'http://deb.debian.org'}|g' ${filePath} || (sudo mkdir -p /etc/apt/sources.list.d && echo -e "Types: deb\\nURIs: ${mirrorDomainWithScheme || 'http://deb.debian.org'}\\nSuites: \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2) \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-updates\\nComponents: main\\nSigned-By: /usr/share/keyrings/debian-archive-keyring.gpg" | sudo tee ${filePath}) && sudo apt-get update`; return `${S}sed -i 's|${currentDomainWithScheme}|${mirrorDomainWithScheme || 'http://deb.debian.org'}|g' ${filePath} || (${S}mkdir -p /etc/apt/sources.list.d && echo -e "Types: deb\\nURIs: ${mirrorDomainWithScheme || 'http://deb.debian.org'}\\nSuites: \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2) \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-updates\\nComponents: main\\nSigned-By: /usr/share/keyrings/debian-archive-keyring.gpg" | ${S}tee ${filePath}) && ${S}apt-get update`;
} else { } else {
return `sudo mkdir -p /etc/apt/sources.list.d && echo -e "Types: deb\\nURIs: ${mirrorDomainWithScheme || 'http://deb.debian.org'}\\nSuites: \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2) \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-updates\\nComponents: main\\nSigned-By: /usr/share/keyrings/debian-archive-keyring.gpg" | sudo tee ${filePath} && sudo apt-get update`; return `${S}mkdir -p /etc/apt/sources.list.d && echo -e "Types: deb\\nURIs: ${mirrorDomainWithScheme || 'http://deb.debian.org'}\\nSuites: \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2) \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-updates\\nComponents: main\\nSigned-By: /usr/share/keyrings/debian-archive-keyring.gpg" | ${S}tee ${filePath} && ${S}apt-get update`;
} }
case 'Ubuntu': case 'Ubuntu':
filePath = '/etc/apt/sources.list.d/ubuntu.sources'; filePath = '/etc/apt/sources.list.d/ubuntu.sources';
currentDomainWithScheme = await getCurrentMirrorDomain(filePath); currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
if (currentDomainWithScheme) { if (currentDomainWithScheme) {
return `sudo sed -i 's|${currentDomainWithScheme}|${mirrorDomainWithScheme || 'http://archive.ubuntu.com'}|g' ${filePath} || (sudo mkdir -p /etc/apt/sources.list.d && echo -e "Types: deb\\nURIs: ${mirrorDomainWithScheme || 'http://archive.ubuntu.com'}\\nSuites: \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2) \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-updates \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-backports\\nComponents: main restricted universe multiverse\\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg" | sudo tee ${filePath}) && sudo apt-get update`; return `${S}sed -i 's|${currentDomainWithScheme}|${mirrorDomainWithScheme || 'http://archive.ubuntu.com'}|g' ${filePath} || (${S}mkdir -p /etc/apt/sources.list.d && echo -e "Types: deb\\nURIs: ${mirrorDomainWithScheme || 'http://archive.ubuntu.com'}\\nSuites: \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2) \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-updates \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-backports\\nComponents: main restricted universe multiverse\\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg" | ${S}tee ${filePath}) && ${S}apt-get update`;
} else { } else {
return `sudo mkdir -p /etc/apt/sources.list.d && echo -e "Types: deb\\nURIs: ${mirrorDomainWithScheme || 'http://archive.ubuntu.com'}\\nSuites: \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2) \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-updates \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-backports\\nComponents: main restricted universe multiverse\\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg" | sudo tee ${filePath} && sudo apt-get update`; return `${S}mkdir -p /etc/apt/sources.list.d && echo -e "Types: deb\\nURIs: ${mirrorDomainWithScheme || 'http://archive.ubuntu.com'}\\nSuites: \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2) \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-updates \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-backports\\nComponents: main restricted universe multiverse\\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg" | ${S}tee ${filePath} && ${S}apt-get update`;
} }
case 'Alpine': case 'Alpine':
filePath = '/etc/apk/repositories'; filePath = '/etc/apk/repositories';

View File

@ -31,6 +31,7 @@ import { writeFileWithLock } from '../shared/utils';
import { t } from '../shared/i18n'; import { t } from '../shared/i18n';
import { ScheduleType } from '../interface/schedule'; import { ScheduleType } from '../interface/schedule';
import { logStreamManager } from '../shared/logStreamManager'; import { logStreamManager } from '../shared/logStreamManager';
import { isEmpty } from 'lodash';
@Service() @Service()
export default class CronService { export default class CronService {
@ -401,7 +402,7 @@ export default class CronService {
} }
private formatFilterQuery(query: any, filterQuery: any) { private formatFilterQuery(query: any, filterQuery: any) {
if (filterQuery) { if (!isEmpty(filterQuery)) {
if (!query[Op.and]) { if (!query[Op.and]) {
query[Op.and] = []; query[Op.and] = [];
} }

View File

@ -17,6 +17,7 @@ ARG PYTHON_SHORT_VERSION=3.11
ENV QL_DIR=/ql \ ENV QL_DIR=/ql \
QL_BRANCH=${QL_BRANCH} \ QL_BRANCH=${QL_BRANCH} \
QL_CONTAINER=true \
LANG=C.UTF-8 \ LANG=C.UTF-8 \
SHELL=/bin/bash \ SHELL=/bin/bash \
PS1="\u@\h:\w \$ " PS1="\u@\h:\w \$ "

View File

@ -17,6 +17,7 @@ ARG PYTHON_SHORT_VERSION=3.10
ENV QL_DIR=/ql \ ENV QL_DIR=/ql \
QL_BRANCH=${QL_BRANCH} \ QL_BRANCH=${QL_BRANCH} \
QL_CONTAINER=true \
LANG=C.UTF-8 \ LANG=C.UTF-8 \
SHELL=/bin/bash \ SHELL=/bin/bash \
PS1="\u@\h:\w \$ " PS1="\u@\h:\w \$ "

View File

@ -22,6 +22,7 @@ ARG PYTHON_SHORT_VERSION=3.11
ENV QL_DIR=/ql \ ENV QL_DIR=/ql \
QL_BRANCH=${QL_BRANCH} \ QL_BRANCH=${QL_BRANCH} \
QL_CONTAINER=true \
LANG=C.UTF-8 \ LANG=C.UTF-8 \
SHELL=/bin/bash \ SHELL=/bin/bash \
PS1="\u@\h:\w \$ " PS1="\u@\h:\w \$ "

View File

@ -22,6 +22,7 @@ ARG PYTHON_SHORT_VERSION=3.10
ENV QL_DIR=/ql \ ENV QL_DIR=/ql \
QL_BRANCH=${QL_BRANCH} \ QL_BRANCH=${QL_BRANCH} \
QL_CONTAINER=true \
LANG=C.UTF-8 \ LANG=C.UTF-8 \
SHELL=/bin/bash \ SHELL=/bin/bash \
PS1="\u@\h:\w \$ " PS1="\u@\h:\w \$ "

View File

@ -367,7 +367,7 @@ const Crontab = () => {
const getCrons = async (silent?: boolean) => { const getCrons = async (silent?: boolean) => {
if (!silent) setLoading(true); if (!silent) setLoading(true);
const { page = 1, size = 10, sorter, filters = '{}' } = pageConf; const { page = 1, size = 20, sorter, filters = {} } = pageConf;
let url = `${config.apiPrefix let url = `${config.apiPrefix
}crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify( }crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify(
filters, filters,