fix(webui-backend): sanitize plugin ID to prevent path injection (CodeQL js/path-injection)

This commit is contained in:
时瑾
2026-02-07 13:52:15 +08:00
parent beef1233fa
commit 3d2fabcfb2

View File

@@ -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<void> {
// 验证 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');