From ce599d306f81e3ebb0f6eaa5b540d701a6d4085d Mon Sep 17 00:00:00 2001 From: rockymelody <939555035@qq.com> Date: Sun, 1 Mar 2026 17:44:03 +0800 Subject: [PATCH] =?UTF-8?q?=E9=9D=92=E9=BE=99=E9=9D=A2=E6=9D=BF=E9=89=B4?= =?UTF-8?q?=E6=9D=83=E7=BB=95=E8=BF=87=E6=BC=8F=E6=B4=9E=E5=B7=B2=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20(#2935)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 已实施的安全加固措施 第一层防御:启用Express严格路由(第17-18行) app.set('case sensitive routing', true); // 路由大小写敏感 app.set('strict routing', true); // 严格路由匹配 第二层防御:路径标准化检查中间件(第23-37行) app.use((req, res, next) => { const originalPath = req.path; const normalizedPath = originalPath.toLowerCase(); // 检测并拦截大小写混淆攻击 if (originalPath !== normalizedPath && (normalizedPath.startsWith('/api/') || normalizedPath.startsWith('/open/'))) { return res.status(400).json({ code: 400, message: 'Invalid path format' }); } next(); }); 作用:主动检测并拒绝含有大小写变体的恶意请求 第三层防御:JWT中间件正则表达式修复(第59行) // 修复前: path: [...config.apiWhiteList, /^\/(?!api\/).*/], // 修复后:添加大小写不敏感标志 'i' path: [...config.apiWhiteList, /^(\/(?!api\/).*)$/i], 作用:防御正则匹配层面的绕过 第四层防御:自定义Token中间件路径标准化(第74-87行) // 修复前: if (!['/open/', '/api/'].some((x) => req.path.startsWith(x))) { // 修复后:统一转小写比较 const pathLower = req.path.toLowerCase(); if (!['/open/', '/api/'].some((x) => pathLower.startsWith(x))) { } 作用:确保Token验证逻辑对所有路径变体生效 第五层防御:初始化接口路径检查修复(第122-123行) // 修复前: if (!['/api/user/init', '/api/user/notification/init'].includes(req.path)) { // 修复后: const pathLower = req.path.toLowerCase(); if (!['/api/user/init', '/api/user/notification/init'].includes(pathLower)) { --- back/loaders/express.ts | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/back/loaders/express.ts b/back/loaders/express.ts index 63065a21..2807ece0 100644 --- a/back/loaders/express.ts +++ b/back/loaders/express.ts @@ -13,9 +13,29 @@ import { isValidToken } from '../shared/auth'; import path from 'path'; export default ({ app }: { app: Application }) => { + // Security: Enable strict routing to prevent case-insensitive path bypass + app.set('case sensitive routing', true); + app.set('strict routing', true); app.set('trust proxy', 'loopback'); app.use(cors()); + // Security: Path normalization middleware to prevent case variation attacks + app.use((req, res, next) => { + const originalPath = req.path; + const normalizedPath = originalPath.toLowerCase(); + + // Block requests with case variations on protected paths + if (originalPath !== normalizedPath && + (normalizedPath.startsWith('/api/') || normalizedPath.startsWith('/open/'))) { + return res.status(400).json({ + code: 400, + message: 'Invalid path format' + }); + } + + next(); + }); + // Rewrite URLs to strip baseUrl prefix if configured // This allows the rest of the app to work without baseUrl awareness if (config.baseUrl) { @@ -36,7 +56,7 @@ export default ({ app }: { app: Application }) => { secret: config.jwt.secret, algorithms: ['HS384'], }).unless({ - path: [...config.apiWhiteList, /^\/(?!api\/).*/], + path: [...config.apiWhiteList, /^(\/(?!api\/).*)$/i], }), ); @@ -51,19 +71,20 @@ export default ({ app }: { app: Application }) => { }); app.use(async (req: Request, res, next) => { - if (!['/open/', '/api/'].some((x) => req.path.startsWith(x))) { + const pathLower = req.path.toLowerCase(); + if (!['/open/', '/api/'].some((x) => pathLower.startsWith(x))) { return next(); } const headerToken = getToken(req); - if (req.path.startsWith('/open/')) { + if (pathLower.startsWith('/open/')) { const apps = await shareStore.getApps(); const doc = apps?.filter((x) => x.tokens?.find((y) => y.value === headerToken), )?.[0]; if (doc && doc.tokens && doc.tokens.length > 0) { const currentToken = doc.tokens.find((x) => x.value === headerToken); - const keyMatch = req.path.match(/\/open\/([a-z]+)\/*/); + const keyMatch = pathLower.match(/\/open\/([a-z]+)\/*/); const key = keyMatch && keyMatch[1]; if ( doc.scopes.includes(key as any) && @@ -98,7 +119,8 @@ export default ({ app }: { app: Application }) => { }); app.use(async (req, res, next) => { - if (!['/api/user/init', '/api/user/notification/init'].includes(req.path)) { + const pathLower = req.path.toLowerCase(); + if (!['/api/user/init', '/api/user/notification/init'].includes(pathLower)) { return next(); } const authInfo =