From 172a75b51410efabfb3f00d85ac85ad3df254542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=97=B6=E7=91=BE?= <74231782+sj817@users.noreply.github.com> Date: Sat, 7 Feb 2026 13:52:15 +0800 Subject: [PATCH] fix(webui-backend): sanitize plugin ID to prevent path injection (CodeQL js/path-injection) --- .../src/api/PluginStore.ts | 44 +++++++++++++++---- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/packages/napcat-webui-backend/src/api/PluginStore.ts b/packages/napcat-webui-backend/src/api/PluginStore.ts index fb8b5679..974f9238 100644 --- a/packages/napcat-webui-backend/src/api/PluginStore.ts +++ b/packages/napcat-webui-backend/src/api/PluginStore.ts @@ -27,6 +27,22 @@ const PLUGIN_STORE_SOURCES = [ // 插件目录 - 使用 pathWrapper const getPluginsDir = () => webUiPathWrapper.pluginPath; +/** + * 验证插件 ID,防止路径注入攻击 + */ +function validatePluginId (id: any): string { + if (typeof id !== 'string') { + throw new Error('Invalid plugin ID'); + } + // 仅允许字母、数字、点、下划线、连字符,禁止路径遍历字符 + // 通过 path.basename 进一步确保不包含路径分隔符 + const safeId = path.basename(id); + if (!/^[a-zA-Z0-9._-]+$/.test(safeId) || safeId !== id) { + throw new Error('Invalid plugin ID format'); + } + return safeId; +} + // 插件列表缓存 let pluginListCache: PluginStoreList | null = null; let cacheTimestamp: number = 0; @@ -197,13 +213,15 @@ async function downloadFile ( * 解压插件到指定目录 */ async function extractPlugin (zipPath: string, pluginId: string): Promise { + // 验证 pluginId 确保安全 + const safeId = validatePluginId(pluginId); const PLUGINS_DIR = getPluginsDir(); - const pluginDir = path.join(PLUGINS_DIR, pluginId); + const pluginDir = path.join(PLUGINS_DIR, safeId); const dataDir = path.join(pluginDir, 'data'); - const tempDataDir = path.join(PLUGINS_DIR, `${pluginId}.data.backup`); + const tempDataDir = path.join(PLUGINS_DIR, `${safeId}.data.backup`); console.log(`[extractPlugin] PLUGINS_DIR: ${PLUGINS_DIR}`); - console.log(`[extractPlugin] pluginId: ${pluginId}`); + console.log(`[extractPlugin] pluginId: ${safeId}`); console.log(`[extractPlugin] Target directory: ${pluginDir}`); console.log(`[extractPlugin] Zip file: ${zipPath}`); @@ -288,7 +306,7 @@ export const GetPluginStoreListHandler: RequestHandler = async (req, res) => { */ export const GetPluginStoreDetailHandler: RequestHandler = async (req, res) => { try { - const { id } = req.params; + const id = validatePluginId(req.params['id']); const data = await fetchPluginList(); const plugin = data.plugins.find(p => p.id === id); @@ -307,12 +325,14 @@ export const GetPluginStoreDetailHandler: RequestHandler = async (req, res) => { */ export const InstallPluginFromStoreHandler: RequestHandler = async (req, res) => { try { - const { id, mirror } = req.body; + const { id: rawId, mirror } = req.body; - if (!id) { + if (!rawId) { return sendError(res, 'Plugin ID is required'); } + const id = validatePluginId(rawId); + // 获取插件信息 const data = await fetchPluginList(); const plugin = data.plugins.find(p => p.id === id); @@ -375,13 +395,21 @@ export const InstallPluginFromStoreHandler: RequestHandler = async (req, res) => * 安装插件(从商店)- SSE 版本,实时推送进度 */ export const InstallPluginFromStoreSSEHandler: RequestHandler = async (req, res) => { - const { id, mirror } = req.query; + const { id: rawId, mirror } = req.query; - if (!id || typeof id !== 'string') { + if (!rawId || typeof rawId !== 'string') { res.status(400).json({ error: 'Plugin ID is required' }); return; } + let id: string; + try { + id = validatePluginId(rawId); + } catch (err: any) { + res.status(400).json({ error: err.message }); + return; + } + // 设置 SSE 响应头 res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache');