From e8c6043a1f95468a534bf15adfbbbdfb230e46c7 Mon Sep 17 00:00:00 2001 From: yuyx <237899745@qq.com> Date: Sun, 30 Nov 2025 13:44:23 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E5=A4=8D=E7=89=B9?= =?UTF-8?q?=E6=AE=8A=E5=AD=97=E7=AC=A6=E6=96=87=E4=BB=B6=E5=90=8D=E7=9A=84?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加decodeHtmlEntities函数解码HTML实体 - 在rename/mkdir/folder-info/delete接口中解码文件名和路径 - 删除操作支持多候选路径,处理二次编码情况 - 移除sanitizeInput中对反引号的转义 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- backend/server.js | 94 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 86 insertions(+), 8 deletions(-) diff --git a/backend/server.js b/backend/server.js index c817d3c..5037814 100644 --- a/backend/server.js +++ b/backend/server.js @@ -206,8 +206,7 @@ function sanitizeInput(str) { '<': '<', '>': '>', '"': '"', - "'": ''', - '`': '`' + "'": ''' }; return map[char]; }); @@ -221,6 +220,46 @@ function sanitizeInput(str) { return sanitized; } +// 将 HTML 实体解码为原始字符(用于文件名/路径字段) +function decodeHtmlEntities(str) { + if (typeof str !== 'string') return str; + + // 支持常见实体和数字实体(含多次嵌套,如 &#x60;) + const entityMap = { + amp: '&', + lt: '<', + gt: '>', + quot: '"', + apos: "'", + '#x27': "'", + '#x2F': '/', + '#x60': '`' + }; + + const decodeOnce = (input) => + input.replace(/&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z]+);/g, (match, code) => { + if (code[0] === '#') { + const isHex = code[1]?.toLowerCase() === 'x'; + const num = isHex ? parseInt(code.slice(2), 16) : parseInt(code.slice(1), 10); + if (!Number.isNaN(num)) { + return String.fromCharCode(num); + } + return match; + } + const mapped = entityMap[code]; + return mapped !== undefined ? mapped : match; + }); + + let output = str; + let decoded = decodeOnce(output); + // 处理嵌套实体(如 &#x60;),直到稳定 + while (decoded !== output) { + output = decoded; + decoded = decodeOnce(output); + } + return output; +} + // HTML转义(用于模板输出) function escapeHtml(str) { if (typeof str !== 'string') return str; @@ -2275,7 +2314,9 @@ app.get('/api/files', authMiddleware, async (req, res) => { // 重命名文件 app.post('/api/files/rename', authMiddleware, async (req, res) => { - const { oldName, newName, path } = req.body; + const oldName = decodeHtmlEntities(req.body.oldName); + const newName = decodeHtmlEntities(req.body.newName); + const path = decodeHtmlEntities(req.body.path) || '/'; let storage; if (!oldName || !newName) { @@ -2313,7 +2354,8 @@ app.post('/api/files/rename', authMiddleware, async (req, res) => { // 创建文件夹 app.post('/api/files/mkdir', authMiddleware, async (req, res) => { - const { path, folderName } = req.body; + const path = decodeHtmlEntities(req.body.path) || '/'; + const folderName = decodeHtmlEntities(req.body.folderName); let storage; // 参数验证 @@ -2380,7 +2422,8 @@ app.post('/api/files/mkdir', authMiddleware, async (req, res) => { // 获取文件夹详情(大小统计) app.post('/api/files/folder-info', authMiddleware, async (req, res) => { - const { path: dirPath, folderName } = req.body; + const dirPath = decodeHtmlEntities(req.body.path) || '/'; + const folderName = decodeHtmlEntities(req.body.folderName); let storage; if (!folderName) { @@ -2475,7 +2518,10 @@ app.post('/api/files/folder-info', authMiddleware, async (req, res) => { // 删除文件 app.post('/api/files/delete', authMiddleware, async (req, res) => { - const { fileName, path } = req.body; + const rawFileName = req.body.fileName; + const rawPath = req.body.path; + const fileName = decodeHtmlEntities(rawFileName); + const path = decodeHtmlEntities(rawPath) || '/'; let storage; if (!fileName) { @@ -2491,9 +2537,41 @@ app.post('/api/files/delete', authMiddleware, async (req, res) => { const storageInterface = new StorageInterface(req.user); storage = await storageInterface.connect(); - const filePath = path === '/' ? `/${fileName}` : `${path}/${fileName}`; + const tried = new Set(); + const candidates = [fileName]; - await storage.delete(filePath); + // 兼容被二次编码的实体(如 &#x60; -> `) + if (typeof rawFileName === 'string') { + const entityName = rawFileName.replace(/&/g, '&'); + if (entityName && !candidates.includes(entityName)) { + candidates.push(entityName); + } + if (rawFileName && !candidates.includes(rawFileName)) { + candidates.push(rawFileName); + } + } + + const pathsToDelete = candidates.map(name => (path === '/' ? `/${name}` : `${path}/${name}`)); + + try { + for (const targetPath of pathsToDelete) { + if (tried.has(targetPath)) continue; + tried.add(targetPath); + try { + await storage.delete(targetPath); + break; + } catch (err) { + if (err.code === 'ENOENT') { + // 尝试下一个候选路径 + if (targetPath === pathsToDelete[pathsToDelete.length - 1]) throw err; + } else { + throw err; + } + } + } + } catch (err) { + throw err; + } res.json({ success: true,