diff --git a/public/index.html b/public/index.html index f729840..10b2ba8 100644 --- a/public/index.html +++ b/public/index.html @@ -5,7 +5,7 @@ - COOL-AMIND 一个很酷的后台权限管理系统 + COOL-ADMIN 一个很酷的后台权限管理系统 diff --git a/src/modules/base/service/sys/log.ts b/src/modules/base/service/sys/log.ts index 8695c92..e2e4195 100644 --- a/src/modules/base/service/sys/log.ts +++ b/src/modules/base/service/sys/log.ts @@ -53,8 +53,8 @@ export class BaseSysLogService extends BaseService { } const keepDay = await this.baseSysConfService.getValue('logKeep'); if (keepDay) { - const beforeDate = moment().add(-keepDay, 'days').startOf('day').toDate(); - await this.baseSysLogEntity.delete({ createTime: LessThan(beforeDate) }); + const beforeDate = moment().add(-keepDay, 'days').startOf('day').format('YYYY-MM-DD HH:mm:ss'); + await this.baseSysLogEntity.createQueryBuilder().delete().where('createTime < :beforeDate', { beforeDate }).execute(); } else { await this.baseSysLogEntity.clear(); } diff --git a/src/modules/plugin/hooks/upload/index.ts b/src/modules/plugin/hooks/upload/index.ts index effb4b7..72a9c4c 100644 --- a/src/modules/plugin/hooks/upload/index.ts +++ b/src/modules/plugin/hooks/upload/index.ts @@ -12,6 +12,49 @@ import { pUploadPath } from '../../../../comm/path'; * 文件上传 */ export class CoolPlugin extends BasePluginHook implements BaseUpload { + /** + * 验证路径安全性,防止路径遍历攻击 + * @param userInput 用户输入的文件名或路径 + * @returns 安全的文件名 + */ + private sanitizePath(userInput: string): string { + if (!userInput) { + return ''; + } + // 检查是否包含路径遍历字符 + if ( + userInput.includes('..') || + userInput.includes('./') || + userInput.includes('.\\') || + userInput.includes('\\') || + userInput.includes('//') || + userInput.includes('\0') || + /^[a-zA-Z]:/.test(userInput) || // Windows绝对路径 + userInput.startsWith('/') + ) { + throw new CoolCommException('非法的文件路径'); + } + // 规范化路径后再次检查 + const normalized = path.normalize(userInput); + if (normalized.includes('..') || normalized.startsWith('/')) { + throw new CoolCommException('非法的文件路径'); + } + return normalized; + } + + /** + * 验证最终路径是否在允许的目录内 + * @param targetPath 目标路径 + * @param basePath 基础路径 + */ + private validateTargetPath(targetPath: string, basePath: string): void { + const resolvedTarget = path.resolve(targetPath); + const resolvedBase = path.resolve(basePath); + if (!resolvedTarget.startsWith(resolvedBase + path.sep)) { + throw new CoolCommException('文件路径超出允许范围'); + } + } + /** * 获得上传模式 * @returns @@ -38,27 +81,40 @@ export class CoolPlugin extends BasePluginHook implements BaseUpload { */ async downAndUpload(url: string, fileName?: string) { const { domain } = this.pluginInfo.config; + const basePath = pUploadPath(); + const dateDir = moment().format('YYYYMMDD'); + // 从url获取扩展名 const extend = path.extname(fileName ? fileName : url); + + // 验证文件名安全性 + let safeFileName: string; + if (fileName) { + safeFileName = this.sanitizePath(fileName); + // 只取文件名部分,去除可能的子目录 + safeFileName = path.basename(safeFileName); + } else { + safeFileName = uuid() + extend; + } + const download = require('download'); // 数据 const data = url.includes('http') ? await download(url) : fs.readFileSync(url); + // 创建文件夹 - const dirPath = path.join(pUploadPath(), `${moment().format('YYYYMMDD')}`); + const dirPath = path.join(basePath, dateDir); if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } - const uuidStr = uuid(); - const name = `${moment().format('YYYYMMDD')}/${ - fileName ? fileName : uuidStr + extend - }`; - fs.writeFileSync( - `${dirPath}/${fileName ? fileName : uuid() + extend}`, - data - ); - return `${domain}/upload/${name}`; + + const targetPath = path.join(dirPath, safeFileName); + // 验证最终路径 + this.validateTargetPath(targetPath, basePath); + + fs.writeFileSync(targetPath, data); + return `${domain}/upload/${dateDir}/${safeFileName}`; } /** @@ -68,21 +124,28 @@ export class CoolPlugin extends BasePluginHook implements BaseUpload { */ async uploadWithKey(filePath: any, key: any) { const { domain } = this.pluginInfo.config; + const basePath = pUploadPath(); + const dateDir = moment().format('YYYYMMDD'); + + // 验证key安全性 + const safeKey = this.sanitizePath(key); + const data = fs.readFileSync(filePath); + + // 构建目标路径 + const targetPath = path.join(basePath, dateDir, safeKey); + const dirPath = path.dirname(targetPath); + + // 验证最终路径 + this.validateTargetPath(targetPath, basePath); + // 如果文件夹不存在则创建 - const dirPath = path.join( - pUploadPath(), - moment().format('YYYYMMDD'), - path.dirname(key) - ); if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } - fs.writeFileSync( - path.join(pUploadPath(), moment().format('YYYYMMDD'), key), - data - ); - return `${domain}/upload/${moment().format('YYYYMMDD')}/${key}`; + + fs.writeFileSync(targetPath, data); + return `${domain}/upload/${dateDir}/${safeKey}`; } /** @@ -94,35 +157,45 @@ export class CoolPlugin extends BasePluginHook implements BaseUpload { const { domain } = this.pluginInfo.config; try { const { key } = ctx.fields; - if ( - key && - (key.includes('..') || - key.includes('./') || - key.includes('\\') || - key.includes('//')) - ) { - throw new CoolCommException('非法的key值'); + const basePath = pUploadPath(); + const dateDir = moment().format('YYYYMMDD'); + + // 验证key安全性 + let safeKey: string | undefined; + if (key) { + safeKey = this.sanitizePath(key); } + if (_.isEmpty(ctx.files)) { throw new CoolCommException('上传文件为空'); } - const basePath = pUploadPath(); const file = ctx.files[0]; - const extension = file.filename.split('.').pop(); - const name = - moment().format('YYYYMMDD') + '/' + (key || `${uuid()}.${extension}`); + // 安全处理原始文件名 + const originalFileName = path.basename(file.filename); + const extension = originalFileName.split('.').pop(); + + const finalName = safeKey || `${uuid()}.${extension}`; + const name = `${dateDir}/${finalName}`; const target = path.join(basePath, name); - const dirPath = path.join(basePath, moment().format('YYYYMMDD')); + + // 验证最终路径 + this.validateTargetPath(target, basePath); + + const dirPath = path.join(basePath, dateDir); if (!fs.existsSync(dirPath)) { - fs.mkdirSync(dirPath); + fs.mkdirSync(dirPath, { recursive: true }); } + const data = fs.readFileSync(file.data); fs.writeFileSync(target, data); return domain + '/upload/' + name; } catch (err) { console.error(err); - throw new CoolCommException('上传失败' + err.message); + if (err instanceof CoolCommException) { + throw err; + } + throw new CoolCommException('上传失败: ' + err.message); } } }