From 4e7151686fea939a37b740ede15825bd1989fb56 Mon Sep 17 00:00:00 2001 From: chaoszhu Date: Sun, 28 Jun 2026 14:59:14 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=89=AB=E6=8F=8F=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/app/socket/docker.js | 18 ++++++++++++++++++ server/app/socket/sftp-v2.js | 37 ++++++++++++++++++++++-------------- server/app/utils/tools.js | 14 ++++++++++++++ server/index.js | 2 +- 4 files changed, 56 insertions(+), 15 deletions(-) diff --git a/server/app/socket/docker.js b/server/app/socket/docker.js index 5bc255a..dda0680 100644 --- a/server/app/socket/docker.js +++ b/server/app/socket/docker.js @@ -51,8 +51,22 @@ function executeDockerLogsCommand(targetSSHClient, command) { }) } +const VALID_CONTAINER_ID = /^[a-zA-Z0-9][a-zA-Z0-9_.\-]{0,127}$/ + +function validateContainerId(containerId) { + if (typeof containerId !== 'string' || !VALID_CONTAINER_ID.test(containerId)) { + throw new Error('invalid container id') + } +} + +function sanitizeTail(tail) { + return Math.min(Math.max(parseInt(tail, 10) || 3000, 1), 100000) +} + async function getDockerLogs(targetSSHClient, containerId, tail = 3000) { try { + validateContainerId(containerId) + tail = sanitizeTail(tail) // 使用专门的日志获取函数,确保能获取到所有日志 const logsData = await executeDockerLogsCommand( targetSSHClient, @@ -73,6 +87,7 @@ async function getDockerLogs(targetSSHClient, containerId, tail = 3000) { async function startDockerContainer(targetSSHClient, containerId) { try { + validateContainerId(containerId) await executeCommand(targetSSHClient, `docker start ${ containerId }`) return { success: true, message: '容器启动成功' } } catch (error) { @@ -83,6 +98,7 @@ async function startDockerContainer(targetSSHClient, containerId) { async function stopDockerContainer(targetSSHClient, containerId) { try { + validateContainerId(containerId) await executeCommand(targetSSHClient, `docker stop ${ containerId }`) return { success: true, message: '容器停止成功' } } catch (error) { @@ -93,6 +109,7 @@ async function stopDockerContainer(targetSSHClient, containerId) { async function restartDockerContainer(targetSSHClient, containerId) { try { + validateContainerId(containerId) await executeCommand(targetSSHClient, `docker restart ${ containerId }`) return { success: true, message: '容器重启成功' } } catch (error) { @@ -103,6 +120,7 @@ async function restartDockerContainer(targetSSHClient, containerId) { async function deleteDockerContainer(targetSSHClient, containerId) { try { + validateContainerId(containerId) await executeCommand(targetSSHClient, `docker rm -f ${ containerId }`) return { success: true, message: '容器删除成功' } } catch (error) { diff --git a/server/app/socket/sftp-v2.js b/server/app/socket/sftp-v2.js index 3b102e1..55e83a4 100644 --- a/server/app/socket/sftp-v2.js +++ b/server/app/socket/sftp-v2.js @@ -12,6 +12,11 @@ const hostListDB = new HostListDB().getInstance() const favoriteSftpDB = new FavoriteSftpDB().getInstance() const { Client: SSHClient } = require('ssh2') +function shellEscape(s) { + // eslint-disable-next-line quotes + return "'" + s.replace(/'/g, "'\\''") + "'" +} + /** * 将 Buffer 解码为字符串 * @param {Buffer} buffer - 要解码的 Buffer @@ -377,7 +382,7 @@ const listenAction = (sftpClient, socket) => { for (const { name } of targets) { const src = rawPath.posix.join(dirPath, name) // cp -r preserves dir/file, will overwrite if exists - const cmd = `cp -r -- "${ src }" "${ destDir }/"` + const cmd = `cp -r -- ${ shellEscape(src) } ${ shellEscape(destDir + '/') }` await execCommand(cmd) } @@ -421,7 +426,7 @@ const listenAction = (sftpClient, socket) => { } else if (type === 'file') { logger.info(`创建文件: ${ targetPath }`) // 创建空文件,使用 touch 命令 - const cmd = `touch "${ targetPath }"` + const cmd = `touch ${ shellEscape(targetPath) }` await execCommand(cmd) socket.emit('create_success', `文件 "${ trimmedName }" 创建成功`) } else { @@ -466,10 +471,10 @@ const listenAction = (sftpClient, socket) => { } // 构建要压缩的文件列表 - const fileNames = targets.map(t => `"${ t.name }"`).join(' ') + const fileNames = targets.map(t => shellEscape(t.name)).join(' ') // 使用 tar 命令压缩 - const tarCmd = `cd "${ dirPath }" && tar -czf "${ trimmedArchiveName }" ${ fileNames }` + const tarCmd = `cd ${ shellEscape(dirPath) } && tar -czf ${ shellEscape(trimmedArchiveName) } ${ fileNames }` logger.info(`开始压缩文件: ${ targets.map(t => t.name).join(', ') } -> ${ trimmedArchiveName }`) await execCommand(tarCmd) @@ -531,26 +536,30 @@ const listenAction = (sftpClient, socket) => { // 根据文件扩展名选择解压命令 let decompressCmd = '' + const escapedDir = shellEscape(dirPath) + const escapedFile = shellEscape(trimmedFileName) + const escapedFolder = folderName ? shellEscape(folderName) : '' + if (/\.tar\.gz$|\.tgz$/i.test(trimmedFileName)) { // tar.gz 或 tgz 格式 if (mode === 'folder') { - decompressCmd = `cd "${ dirPath }" && tar -xzf "${ trimmedFileName }" -C "${ folderName }"` + decompressCmd = `cd ${ escapedDir } && tar -xzf ${ escapedFile } -C ${ escapedFolder }` } else { - decompressCmd = `cd "${ dirPath }" && tar -xzf "${ trimmedFileName }"` + decompressCmd = `cd ${ escapedDir } && tar -xzf ${ escapedFile }` } } else if (/\.tar$/i.test(trimmedFileName)) { // tar 格式 if (mode === 'folder') { - decompressCmd = `cd "${ dirPath }" && tar -xf "${ trimmedFileName }" -C "${ folderName }"` + decompressCmd = `cd ${ escapedDir } && tar -xf ${ escapedFile } -C ${ escapedFolder }` } else { - decompressCmd = `cd "${ dirPath }" && tar -xf "${ trimmedFileName }"` + decompressCmd = `cd ${ escapedDir } && tar -xf ${ escapedFile }` } } else if (/\.zip$/i.test(trimmedFileName)) { // zip 格式 if (mode === 'folder') { - decompressCmd = `cd "${ dirPath }" && unzip -o "${ trimmedFileName }" -d "${ folderName }"` + decompressCmd = `cd ${ escapedDir } && unzip -o ${ escapedFile } -d ${ escapedFolder }` } else { - decompressCmd = `cd "${ dirPath }" && unzip -o "${ trimmedFileName }"` + decompressCmd = `cd ${ escapedDir } && unzip -o ${ escapedFile }` } } @@ -615,7 +624,7 @@ const listenAction = (sftpClient, socket) => { // 在远端打包 logger.info(`开始打包文件夹: ${ srcPath }`) - const tarCmd = `cd "${ dirPath }" && tar -czf "${ remoteTarPath }" "${ target.name }"` + const tarCmd = `cd ${ shellEscape(dirPath) } && tar -czf ${ shellEscape(remoteTarPath) } ${ shellEscape(target.name) }` try { await execCommand(tarCmd) logger.info(`打包文件夹: ${ srcPath } 成功`) @@ -666,8 +675,8 @@ const listenAction = (sftpClient, socket) => { } // 构建tar命令,打包所有选中的文件/文件夹 - const fileNames = targets.map(t => `"${ t.name }"`).join(' ') - const tarCmd = `cd "${ dirPath }" && tar -czf "${ remoteTarPath }" ${ fileNames }` + const fileNames = targets.map(t => shellEscape(t.name)).join(' ') + const tarCmd = `cd ${ shellEscape(dirPath) } && tar -czf ${ shellEscape(remoteTarPath) } ${ fileNames }` logger.info(`开始打包多个文件: ${ targets.map(t => t.name).join(', ') }`) try { @@ -713,7 +722,7 @@ const listenAction = (sftpClient, socket) => { async function cleanupRemoteTarFile(remoteTarPath) { if (!remoteTarPath) return try { - await execCommand(`rm -f "${ remoteTarPath }"`) + await execCommand(`rm -f ${ shellEscape(remoteTarPath) }`) logger.info(`已清理远程临时文件: ${ remoteTarPath }`) } catch (cleanupErr) { logger.warn('清理远程临时文件失败:', remoteTarPath, cleanupErr.message) diff --git a/server/app/utils/tools.js b/server/app/utils/tools.js index b5f6127..ef58081 100644 --- a/server/app/utils/tools.js +++ b/server/app/utils/tools.js @@ -328,8 +328,22 @@ const isAllowedIp = (requestIP) => { return flag } +const VALID_HOSTNAME = /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/ + +function isValidPingTarget(ip) { + if (typeof ip !== 'string') return false + const trimmed = ip.trim() + if (!trimmed || trimmed.length > 253) return false + if (net.isIP(trimmed)) return true + return VALID_HOSTNAME.test(trimmed) +} + const ping = (ip, timeout = 5000) => { return new Promise((resolve) => { + if (!isValidPingTarget(ip)) { + return resolve({ success: false, msg: 'invalid host' }) + } + ip = ip.trim() setTimeout(() => { resolve({ success: false, msg: 'ping timeout!' }) }, timeout) diff --git a/server/index.js b/server/index.js index 0a2c2f4..3b1fe37 100644 --- a/server/index.js +++ b/server/index.js @@ -1,3 +1,3 @@ -global.rpdToken = Array.from({ length:32 },()=>Math.random().toString(36)[2]).join('') +global.rpdToken = require('crypto').randomBytes(32).toString('hex') require('dotenv').config() require('./app/main.js')