diff --git a/packages/napcat-webui-backend/src/api/BackupConfig.ts b/packages/napcat-webui-backend/src/api/BackupConfig.ts index fa75b4ec..13b4e9e6 100644 --- a/packages/napcat-webui-backend/src/api/BackupConfig.ts +++ b/packages/napcat-webui-backend/src/api/BackupConfig.ts @@ -1,6 +1,7 @@ import { RequestHandler } from 'express'; import { existsSync, createReadStream, mkdirSync, rmSync, cpSync, readdirSync } from 'node:fs'; -import { join } from 'node:path'; +import { join, normalize } from 'node:path'; +import os from 'node:os'; import { webUiPathWrapper } from '@/napcat-webui-backend/index'; import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data'; import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response'; @@ -60,16 +61,8 @@ export const BackupExportConfigHandler: RequestHandler = async (_req, res) => { } }; -// 导入配置 -// 将上传的zip文件解压到工作目录下的tmp目录,然后覆盖到config文件夹 +// 导入配置,将上传的zip文件解压到工作目录下的tmp目录,然后覆盖到config文件夹 export const BackupImportConfigHandler: RequestHandler = async (req, res) => { - // 获取QQ登录状态 - const isLogin = WebUiDataRuntime.getQQLoginStatus(); - // 如果未登录,返回错误 - if (!isLogin) { - return sendError(res, 'Not Login'); - } - // 检查是否有文件上传 if (!req.file) { return sendError(res, '请选择要导入的配置文件'); @@ -77,8 +70,8 @@ export const BackupImportConfigHandler: RequestHandler = async (req, res) => { try { const configPath = webUiPathWrapper.configPath; - const tmpPath = join(webUiPathWrapper.cachePath, './tmp'); - const backupRootPath = join(webUiPathWrapper.cachePath, './backup'); + const tmpPath = join(os.tmpdir(), 'napcat-upload', 'tmp'); + const backupRootPath = join(os.tmpdir(), 'napcat-upload', 'backup'); let extractPath = join(tmpPath, 'imported_config'); const uploadedFilePath = req.file.path; @@ -129,6 +122,14 @@ export const BackupImportConfigHandler: RequestHandler = async (req, res) => { for (const entry of entries) { const srcPath = join(src, entry.name); const destPath = join(dest, entry.name); + + // 防止路径穿越攻击 + const normalizedDestPath = normalize(destPath); + const normalizedDestDir = normalize(dest); + if (!normalizedDestPath.startsWith(normalizedDestDir)) { + continue; + } + if (entry.isDirectory()) { mkdirSync(destPath, { recursive: true }); copyRecursive(srcPath, destPath); @@ -146,6 +147,14 @@ export const BackupImportConfigHandler: RequestHandler = async (req, res) => { for (const entry of entries) { const srcPath = join(src, entry.name); const destPath = join(dest, entry.name); + + // 防止路径穿越攻击 + const normalizedDestPath = normalize(destPath); + const normalizedDestDir = normalize(dest); + if (!normalizedDestPath.startsWith(normalizedDestDir)) { + continue; + } + if (entry.isDirectory()) { mkdirSync(destPath, { recursive: true }); copyRecursive(srcPath, destPath); @@ -156,16 +165,13 @@ export const BackupImportConfigHandler: RequestHandler = async (req, res) => { }; copyRecursive(extractPath, configPath); - // 清理临时文件 - rmSync(join(tmpPath, 'imported_config'), { recursive: true, force: true }); - rmSync(uploadedFilePath, { force: true }); - return sendSuccess(res, { message: '配置导入成功,重启后生效~', backupPath: backupPath }); } catch (error) { + console.error('导入配置失败:', error); const msg = (error as Error).message; return sendError(res, `导入配置失败: ${msg}`); } diff --git a/packages/napcat-webui-backend/src/helper/config.ts b/packages/napcat-webui-backend/src/helper/config.ts index 42f486a5..01e36ed7 100644 --- a/packages/napcat-webui-backend/src/helper/config.ts +++ b/packages/napcat-webui-backend/src/helper/config.ts @@ -32,6 +32,8 @@ const WebUiConfigSchema = Type.Object({ ipBlacklist: Type.Array(Type.String(), { default: [] }), // 是否启用 X-Forwarded-For 获取真实IP enableXForwardedFor: Type.Boolean({ default: false }), + // 上传文件大小限制(MB) + uploadSizeLimit: Type.Number({ default: 50 }), }); export type WebUiConfigType = Static; diff --git a/packages/napcat-webui-backend/src/router/OB11Config.ts b/packages/napcat-webui-backend/src/router/OB11Config.ts index cc40cc91..ed5d5c59 100644 --- a/packages/napcat-webui-backend/src/router/OB11Config.ts +++ b/packages/napcat-webui-backend/src/router/OB11Config.ts @@ -1,17 +1,35 @@ import { Router } from 'express'; import multer from 'multer'; import { join } from 'node:path'; -import { webUiPathWrapper } from '@/napcat-webui-backend/index'; +import os from 'node:os'; +import { WebUiConfig } from '@/napcat-webui-backend/index'; import { OB11GetConfigHandler, OB11SetConfigHandler } from '@/napcat-webui-backend/src/api/OB11Config'; import { BackupExportConfigHandler, BackupImportConfigHandler } from '@/napcat-webui-backend/src/api/BackupConfig'; const router: Router = Router(); -// 延迟初始化multer配置,避免在webUiPathWrapper初始化前访问 +// 延迟初始化multer配置 const getUpload = () => { - const tmpPath = join(webUiPathWrapper.cachePath, './tmp'); - return multer({ dest: tmpPath }); + // 使用系统临时目录作为基础路径,方便多个napcat用户统一读取使用 + const tmpPath = join(os.tmpdir(), 'napcat-upload'); + // 获取上传大小限制,默认50MB,最大200MB + let uploadSizeLimit = 50; + try { + // 使用同步方式获取配置 + uploadSizeLimit = WebUiConfig?.WebUiConfigData?.uploadSizeLimit || 50; + } catch (error) { + // 如果获取失败,使用默认值 + console.warn('获取上传大小限制失败:', error); + } + // 确保不超过最大限制 + uploadSizeLimit = Math.min(uploadSizeLimit, 200); + return multer({ + dest: tmpPath, + limits: { + fileSize: uploadSizeLimit * 1024 * 1024 // 转换为字节 + } + }); }; // router:读取配置 diff --git a/packages/napcat-webui-frontend/src/pages/dashboard/config/backup.tsx b/packages/napcat-webui-frontend/src/pages/dashboard/config/backup.tsx index c2ff68c4..50cd417f 100644 --- a/packages/napcat-webui-frontend/src/pages/dashboard/config/backup.tsx +++ b/packages/napcat-webui-frontend/src/pages/dashboard/config/backup.tsx @@ -1,8 +1,7 @@ import { Button } from '@heroui/button'; import toast from 'react-hot-toast'; import { LuDownload, LuUpload } from 'react-icons/lu'; - -import key from '@/const/key'; +import { requestServerWithFetch } from '@/utils/request'; // 导入配置 const handleImportConfig = async (event: React.ChangeEvent) => { @@ -19,15 +18,8 @@ const handleImportConfig = async (event: React.ChangeEvent) => const formData = new FormData(); formData.append('configFile', file); - const token = localStorage.getItem(key.token); - const headers: HeadersInit = {}; - if (token) { - headers['Authorization'] = `Bearer ${JSON.parse(token)}`; - } - - const response = await fetch('/api/OB11Config/ImportConfig', { + const response = await requestServerWithFetch('/OB11Config/ImportConfig', { method: 'POST', - headers, body: formData, }); @@ -37,7 +29,12 @@ const handleImportConfig = async (event: React.ChangeEvent) => } const result = await response.json(); - toast.success(result.data?.message || '配置导入成功。'); + // 检查是否成功导入 + if (result.code === 0) { + toast.success(result.data?.message || '配置导入成功。'); + } else { + toast.error(`配置导入失败: ${result.data?.message || '未知错误'}`); + } } catch (error) { const msg = (error as Error).message; @@ -51,16 +48,8 @@ const handleImportConfig = async (event: React.ChangeEvent) => // 导出配置 const handleExportConfig = async () => { try { - const token = localStorage.getItem(key.token); - const headers: HeadersInit = { - 'Content-Type': 'application/json', - }; - if (token) { - headers['Authorization'] = `Bearer ${JSON.parse(token)}`; - } - const response = await fetch('/api/OB11Config/ExportConfig', { + const response = await requestServerWithFetch('/OB11Config/ExportConfig', { method: 'GET', - headers, }); if (!response.ok) {