文章总结: 文章提出用AST语义分析替代传统正则匹配提取Node.js前端路由,可精准识别动态与隐藏路由并输出完整端点、参数、中间件及验证规则,显著提升资产收敛效率与攻击面发现能力,附完整Babel提取器代码可直接复现。 综合评分: 88 文章分类: 安全工具,代码审计,应用安全,WEB安全,实战经验
基于AST的资产路由端点提取新方案
原创
鉴帷安全
鉴帷安全
2026年1月6日 15:39 湖北
目前市面上大多数都是基于正则匹配的路由提取方案,这对于一些动态路由来说,已经完全不够用了,会有极高的错误率和无法匹配的情况,而基于AST的前端路由提取方案,则能更好的匹配这些动态路由与隐藏路由,我们先来看一个例子。
前置需求
环境:Node.js,babel模块
IDEA:WebStorm
OS:windows,其余系统未测试,出问题自行解决
exmple:
const express = require('express');const router = express.Router();const createRoutes = (versions) => { versions.forEach(ver => { router.get(`/v${ver}/:entity`, validate(ver), (req, res) => { const { entity } = req.params; const ids = req.query.ids?.split(',').map(Number) || []; res.json({ entity, version: ver, ids }); }); });};const validate = (version) => (req, res, next) => { const regex = version >= 2 ? /^[A-Z]\d{3}$/ : /^\w{4,8}$/; if (!regex.test(req.params.entity)) { return res.status(400).send('实体格式错误'); } next();};createRoutes([1, 2]);router.param('entity', (req, res, next, value) => { req.entity = { raw: value, normalized: value.toLowerCase() }; next();});module.exports = router;
如果我们用正则来匹配路由端点的话,是无法匹配到所有路由的,比如这样:
/\/(v[12])\/:entity/g
对于收敛暴露面和发现更多资产而言,这将遗留很大一部分接口,同时,在正则表达式情况下,需要人工去复核的JS代码中的路由情况,也会增加额外的工作量,而AST技术,基于语义分析,可以很好的提取这些路由API和端点。
const parser = require('@babel/parser');const traverse = require('@babel/traverse').default;const t = require('@babel/types');const generator = require('@babel/generator').default;const fs = require('fs');class AdvancedASTRouteExtractor { constructor() { this.routes = []; this.middlewares = new Map(); this.routerDeclarations = new Map(); this.functionDefinitions = new Map(); this.imports = new Map(); this.currentFile = null; } parseCode(code, filename = null) { this.currentFile = filename; const ast = parser.parse(code, { sourceType: 'module', plugins: [ 'jsx', 'typescript', 'asyncGenerators', 'classProperties', 'decorators-legacy', 'dynamicImport', 'exportDefaultFrom', 'exportNamespaceFrom', 'functionBind', 'functionSent', 'nullishCoalescingOperator', 'numericSeparator', 'objectRestSpread', 'optionalCatchBinding', 'optionalChaining', 'throwExpressions' ] }); // 分阶段分析 this.analyzeImports(ast); this.analyzeFunctionDefinitions(ast); this.analyzeRouterDeclarations(ast); this.analyzeRouteDefinitions(ast); this.analyzeDynamicRoutes(ast); this.resolveDependencies(); return this.generateCompleteReport(); } analyzeImports(ast) { traverse(ast, { ImportDeclaration: (path) => { const source = path.node.source.value; path.node.specifiers.forEach(specifier => { if (t.isImportDefaultSpecifier(specifier)) { this.imports.set(specifier.local.name, { type: 'default', source, name: specifier.local.name }); } else if (t.isImportSpecifier(specifier)) { this.imports.set(specifier.local.name, { type: 'named', source, name: specifier.imported.name, local: specifier.local.name }); } }); }, VariableDeclaration: (path) => { // 处理 require 导入 path.node.declarations.forEach(declaration => { if (t.isVariableDeclarator(declaration) && t.isCallExpression(declaration.init) && t.isIdentifier(declaration.init.callee, { name: 'require' })) { const source = declaration.init.arguments[0]?.value; const varName = declaration.id.name; this.imports.set(varName, { type: 'require', source, name: varName }); } }); } }); } analyzeFunctionDefinitions(ast) { traverse(ast, { FunctionDeclaration: (path) => { const funcName = path.node.id?.name; if (funcName) { this.functionDefinitions.set(funcName, { type: 'function', node: path.node, params: this.extractFunctionParams(path.node), body: path.node.body, location: path.node.loc }); } }, VariableDeclaration: (path) => { path.node.declarations.forEach(declaration => { if (t.isVariableDeclarator(declaration) && (t.isArrowFunctionExpression(declaration.init) || t.isFunctionExpression(declaration.init))) { const funcName = declaration.id.name; const funcNode = declaration.init; this.functionDefinitions.set(funcName, { type: declaration.init.type === 'ArrowFunctionExpression' ? 'arrow' : 'function', node: funcNode, params: this.extractFunctionParams(funcNode), body: funcNode.body, location: funcNode.loc }); } }); } }); } analyzeRouterDeclarations(ast) { traverse(ast, { CallExpression: (path) => { // 查找 const router = express.Router() 或类似 const node = path.node; if (t.isMemberExpression(node.callee) && (t.isIdentifier(node.callee.object) || t.isCallExpression(node.callee.object)) && t.isIdentifier(node.callee.property, { name: 'Router' })) { // 向上查找变量声明 let parent = path.parentPath; while (parent && !t.isVariableDeclaration(parent.node)) { parent = parent.parentPath; } if (parent && t.isVariableDeclaration(parent.node)) { parent.node.declarations.forEach(declaration => { if (t.isVariableDeclarator(declaration) && declaration.init === node && t.isIdentifier(declaration.id)) { this.routerDeclarations.set(declaration.id.name, { type: 'express', node: node, location: node.loc }); } }); } } } }); } analyzeRouteDefinitions(ast) { traverse(ast, { CallExpression: (path) => { const node = path.node; // 处理 router.METHOD(path, ...handlers) if (t.isMemberExpression(node.callee) && t.isIdentifier(node.callee.object)) { const routerName = node.callee.object.name; if (this.routerDeclarations.has(routerName) || this.imports.has(routerName) && this.imports.get(routerName).source === 'express') { const method = node.callee.property.name; const httpMethods = new Set([ 'get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'all', 'use' ]); if (httpMethods.has(method)) { this.processRouteCall(node, method, routerName, path); } } } // 处理函数调用生成路由,如 createRoutes([1, 2]) if (t.isCallExpression(node.callee) && t.isIdentifier(node.callee)) { const funcName = node.callee.name; if (this.functionDefinitions.has(funcName)) { this.processRouteGenerator(node, funcName, path); } } } }); } processRouteCall(node, method, routerName, path) { if (node.arguments.length === 0) return; const pathArg = node.arguments[0]; const handlers = node.arguments.slice(1); // 提取路径 const pathInfo = this.extractPathValue(pathArg); if (!pathInfo) return; // 提取中间件信息 const middlewareInfo = this.extractMiddlewareInfo(handlers); // 提取参数信息 const paramInfo = this.extractParameterInfo(pathInfo.value); // 分析处理函数 const handlerAnalysis = this.analyzeRouteHandlers(handlers); const route = { method: method.toUpperCase(), path: pathInfo.value, originalPath: pathInfo.original, router: routerName, parameters: paramInfo.pathParams, queryParameters: paramInfo.queryParams, middleware: middlewareInfo.middlewares, handlers: handlerAnalysis, validations: middlewareInfo.validations, responses: handlerAnalysis.responses, location: node.loc, type: pathInfo.isDynamic ? 'dynamic_template' : 'static', file: this.currentFile }; this.routes.push(route); } processRouteGenerator(node, funcName, path) { const funcDef = this.functionDefinitions.get(funcName); if (!funcDef) return; // 分析函数定义,查找路由创建模式 traverse(funcDef.node, { CallExpression: (innerPath) => { const innerNode = innerPath.node; // 查找 router.METHOD 调用 if (t.isMemberExpression(innerNode.callee) && t.isIdentifier(innerNode.callee.object)) { const routerName = innerNode.callee.object.name; const method = innerNode.callee.property.name; if (this.routerDeclarations.has(routerName)) { // 检查是否在循环中 let parentPath = innerPath.parentPath; let isInLoop = false; let loopInfo = null; while (parentPath) { if (t.isForEachStatement(parentPath.node) || t.isForStatement(parentPath.node) || t.isWhileStatement(parentPath.node) || t.isForOfStatement(parentPath.node) || t.isForInStatement(parentPath.node)) { isInLoop = true; loopInfo = this.analyzeLoop(parentPath.node); break; } parentPath = parentPath.parentPath; } if (isInLoop) { this.processDynamicRouteInLoop(innerNode, method, routerName, loopInfo, node.arguments, path); } } } } }, funcDef.node); } analyzeLoop(loopNode) { if (t.isForEachStatement(loopNode)) { return { type: 'forEach', array: loopNode.callee.object, param: loopNode.params[0]?.name, body: loopNode.body }; } else if (t.isForStatement(loopNode)) { return { type: 'for', init: loopNode.init, test: loopNode.test, update: loopNode.update, body: loopNode.body }; } return null; } processDynamicRouteInLoop(node, method, routerName, loopInfo, generatorArgs, originalPath) { // 尝试静态分析循环 if (loopInfo.type === 'forEach') { // 分析数组 const arrayValue = this.evaluateStaticExpression(loopInfo.array, generatorArgs); if (arrayValue && Array.isArray(arrayValue)) { arrayValue.forEach(item => { this.processDynamicRouteItem(node, method, routerName, loopInfo.param, item, originalPath); }); } } } processDynamicRouteItem(node, method, routerName, paramName, paramValue, originalArgs) { const pathArg = node.arguments[0]; const handlers = node.arguments.slice(1); // 替换模板中的变量 let resolvedPath = this.extractPathValue(pathArg).value; if (paramName) { resolvedPath = resolvedPath.replace(`\${${paramName}}`, paramValue); resolvedPath = resolvedPath.replace(`\$${paramName}`, paramValue); } const middlewareInfo = this.extractMiddlewareInfo(handlers); const paramInfo = this.extractParameterInfo(resolvedPath); const handlerAnalysis = this.analyzeRouteHandlers(handlers); // 替换中间件中的变量引用 const processedMiddleware = this.processMiddlewareWithParam(middlewareInfo.middlewares, paramName, paramValue); const route = { method: method.toUpperCase(), path: resolvedPath, originalPath: this.extractPathValue(pathArg).original, router: routerName, parameters: paramInfo.pathParams, queryParameters: paramInfo.queryParams, middleware: processedMiddleware, handlers: handlerAnalysis, validations: middlewareInfo.validations, responses: handlerAnalysis.responses, location: node.loc, type: 'generated', generator: { param: paramName, value: paramValue, source: originalArgs }, file: this.currentFile }; this.routes.push(route); } extractPathValue(node) { if (t.isStringLiteral(node)) { return { value: node.value, original: `'${node.value}'`, isDynamic: false }; } if (t.isTemplateLiteral(node)) { const templateValue = this.generateTemplateLiteral(node); return { value: templateValue, original: this.generateCode(node), isDynamic: node.expressions.length > 0 }; } if (t.isBinaryExpression(node) && node.operator === '+') { // 拼接的路径 const left = this.extractPathValue(node.left); const right = this.extractPathValue(node.right); if (left && right) { return { value: left.value + right.value, original: `${left.original} + ${right.original}`, isDynamic: left.isDynamic || right.isDynamic }; } } return { value: this.generateCode(node), original: this.generateCode(node), isDynamic: true }; } generateTemplateLiteral(node) { let parts = []; node.quasis.forEach((quasi, index) => { parts.push(quasi.value.raw); if (index < node.expressions.length) { const exprCode = this.generateCode(node.expressions[index]); parts.push(`{${exprCode}}`); } }); return parts.join(''); } extractParameterInfo(path) { const pathParams = []; const queryParams = new Set(); // 提取路径参数 :param const pathParts = path.split('/'); pathParts.forEach(part => { if (part.startsWith(':')) { pathParams.push({ name: part.substring(1), type: 'path', required: true, location: 'path', pattern: null }); } }); return { pathParams, queryParams: Array.from(queryParams).map(name => ({ name, type: 'query', required: false, location: 'query' })) }; } extractMiddlewareInfo(handlerNodes) { const middlewares = []; const validations = []; handlerNodes.forEach(handler => { if (t.isIdentifier(handler)) { const handlerName = handler.name; // 检查是否是已定义的函数 if (this.functionDefinitions.has(handlerName)) { const funcDef = this.functionDefinitions.get(handlerName); const funcValidations = this.analyzeFunctionValidations(funcDef); middlewares.push({ name: handlerName, type: 'function', definition: funcDef, validations: funcValidations }); validations.push(...funcValidations); } else { middlewares.push({ name: handlerName, type: 'unknown' }); } } else if (t.isCallExpression(handler)) { // 中间件调用,如 validate(ver) middlewares.push({ type: 'call', expression: this.generateCode(handler), callee: this.generateCode(handler.callee) }); } else if (t.isArrowFunctionExpression(handler) || t.isFunctionExpression(handler)) { // 内联函数 const inlineValidations = this.analyzeInlineValidations(handler); middlewares.push({ type: 'inline', params: handler.params.map(p => this.generateCode(p)), validations: inlineValidations }); validations.push(...inlineValidations); } }); return { middlewares, validations }; } analyzeFunctionValidations(funcDef) { const validations = []; if (!funcDef.body || !t.isBlockStatement(funcDef.body)) { return validations; } traverse(funcDef.body, { IfStatement: (path) => { const validation = this.extractValidationFromIf(path.node); if (validation) { validations.push(validation); } }, ReturnStatement: (path) => { if (path.node.argument && t.isCallExpression(path.node.argument) && t.isMemberExpression(path.node.argument.callee) && t.isIdentifier(path.node.argument.callee.object, { name: 'res' })) { // 检查是否返回错误响应 const method = path.node.argument.callee.property.name; if (method === 'status' || method === 'send' || method === 'json') { validations.push({ type: 'error_response', method: method, condition: 'unknown', location: path.node.loc }); } } } }, funcDef.body); return validations; } extractValidationFromIf(ifNode) { const condition = ifNode.test; const consequent = ifNode.consequent; // 正则测试 if (t.isCallExpression(condition) && t.isMemberExpression(condition.callee) && t.isCallExpression(condition.callee.object)) { const testCall = condition.callee.object; if (t.isMemberExpression(testCall.callee) && t.isRegExpLiteral(testCall.callee.object)) { const regex = testCall.callee.object.pattern; const errorMessage = this.extractErrorMessage(consequent); return { type: 'regex', pattern: regex, message: errorMessage, location: ifNode.loc }; } } // 简单的布尔表达式 if (t.isUnaryExpression(condition) && condition.operator === '!') { const inner = condition.argument; if (t.isCallExpression(inner) && t.isMemberExpression(inner.callee) && t.isRegExpLiteral(inner.callee.object)) { const regex = inner.callee.object.pattern; const errorMessage = this.extractErrorMessage(consequent); return { type: 'regex', pattern: regex, message: errorMessage, location: ifNode.loc }; } } return null; } extractErrorMessage(consequent) { if (t.isBlockStatement(consequent)) { const statements = consequent.body; for (const stmt of statements) { if (t.isReturnStatement(stmt) && stmt.argument && t.isCallExpression(stmt.argument) && t.isMemberExpression(stmt.argument.callee) && t.isIdentifier(stmt.argument.callee.object, { name: 'res' })) { const method = stmt.argument.callee.property.name; const args = stmt.argument.arguments; if (method === 'send' && args.length > 0) { return this.extractStringValue(args[0]); } else if (method === 'status' && t.isCallExpression(stmt.argument.callee.object) && t.isMemberExpression(stmt.argument.callee.object.callee) && t.isIdentifier(stmt.argument.callee.object.callee.object, { name: 'res' }) && t.isIdentifier(stmt.argument.callee.object.callee.property, { name: 'status' })) { const statusArgs = stmt.argument.callee.object.arguments; const sendArgs = args; if (sendArgs.length > 0) { return this.extractStringValue(sendArgs[0]); } } } } } return null; } analyzeInlineValidations(funcNode) { const validations = []; if (!t.isBlockStatement(funcNode.body)) { return validations; } traverse(funcNode.body, { IfStatement: (path) => { const validation = this.extractValidationFromIf(path.node); if (validation) { validations.push(validation); } } }, funcNode.body); return validations; } analyzeRouteHandlers(handlerNodes) { const handlers = []; const responses = []; handlerNodes.forEach((handler, index) => { if (t.isArrowFunctionExpression(handler) || t.isFunctionExpression(handler)) { const handlerAnalysis = this.analyzeHandler(handler, index); handlers.push(handlerAnalysis); if (handlerAnalysis.responses) { responses.push(...handlerAnalysis.responses); } } else if (t.isIdentifier(handler)) { handlers.push({ type: 'named_middleware', name: handler.name, index: index }); } }); return { handlers, responses }; } analyzeHandler(handler, index) { const responses = []; const queryParams = new Set(); const bodyParams = new Set(); if (!t.isBlockStatement(handler.body)) { return { type: 'arrow_function', params: handler.params.map(p => this.generateCode(p)), index: index, body: this.generateCode(handler.body) }; } traverse(handler.body, { CallExpression: (path) => { const node = path.node; // 提取响应 if (t.isMemberExpression(node.callee) && t.isIdentifier(node.callee.object, { name: 'res' })) { const method = node.callee.property.name; if (method === 'json') { const data = this.extractResponseData(node.arguments[0]); responses.push({ type: 'success', method: 'json', data: data, location: node.loc }); } else if (method === 'send') { responses.push({ type: 'raw', method: 'send', location: node.loc }); } } else if (t.isCallExpression(node.callee) && t.isMemberExpression(node.callee.object) && t.isCallExpression(node.callee.object.callee) && t.isMemberExpression(node.callee.object.callee.callee) && t.isIdentifier(node.callee.object.callee.callee.object, { name: 'res' }) && t.isIdentifier(node.callee.object.callee.callee.property, { name: 'status' })) { const statusArg = node.callee.object.callee.arguments[0]; const method = node.callee.property.name; if (method === 'json') { const data = this.extractResponseData(node.arguments[0]); responses.push({ type: 'error', status: statusArg.value, method: 'json', data: data, location: node.loc }); } } // 提取参数使用 if (t.isMemberExpression(node.callee) && t.isMemberExpression(node.callee.object) && t.isIdentifier(node.callee.object.object)) { const objName = node.callee.object.object.name; const propName = node.callee.object.property.name; const methodName = node.callee.property.name; if (objName === 'req') { if (propName === 'query') { // 查询参数 const paramName = methodName; queryParams.add(paramName); } else if (propName === 'params') { // 路径参数已在别处处理 } else if (propName === 'body') { // 请求体参数 bodyParams.add(methodName); } } } }, MemberExpression: (path) => { const node = path.node; // 提取 req.query.param 访问 if (t.isMemberExpression(node.object) && t.isIdentifier(node.object.object, { name: 'req' }) && t.isIdentifier(node.object.property, { name: 'query' }) && t.isIdentifier(node.property)) { queryParams.add(node.property.name); } } }, handler.body); return { type: 'function', params: handler.params.map(p => ({ name: this.generateCode(p), type: this.inferParamType(p) })), index: index, queryParams: Array.from(queryParams), bodyParams: Array.from(bodyParams), responses: responses, body: this.generateCode(handler.body) }; } extractResponseData(dataNode) { if (!dataNode) return null; if (t.isObjectExpression(dataNode)) { const result = {}; dataNode.properties.forEach(prop => { if (t.isObjectProperty(prop)) { const key = prop.key.name || prop.key.value; result[key] = this.extractResponseValue(prop.value); } }); return result; } else if (t.isIdentifier(dataNode)) { return { type: 'variable', name: dataNode.name }; } else if (t.isMemberExpression(dataNode)) { return { type: 'member', expression: this.generateCode(dataNode) }; } else if (t.isStringLiteral(dataNode)) { return dataNode.value; } else if (t.isNumericLiteral(dataNode)) { return dataNode.value; } return { type: 'expression', value: this.generateCode(dataNode) }; } extractResponseValue(valueNode) { if (t.isStringLiteral(valueNode)) return valueNode.value; if (t.isNumericLiteral(valueNode)) return valueNode.value; if (t.isBooleanLiteral(valueNode)) return valueNode.value; if (t.isNullLiteral(valueNode)) return null; if (t.isIdentifier(valueNode)) return `$${valueNode.name}`; if (t.isMemberExpression(valueNode)) return this.generateCode(valueNode); if (t.isCallExpression(valueNode)) return `${this.generateCode(valueNode.callee)}(...)`; return 'unknown'; } analyzeDynamicRoutes(ast) { // 保留方法 } resolveDependencies() { // 解析中间件和函数的依赖关系 this.routes.forEach(route => { route.middleware.forEach(mw => { if (mw.type === 'function' && mw.definition) { // 中间件依赖分析 } }); }); } processMiddlewareWithParam(middlewares, paramName, paramValue) { return middlewares.map(mw => { if (mw.validations && mw.validations.length > 0) { const updatedValidations = mw.validations.map(validation => { if (validation.type === 'regex' && paramName) { // 正则依赖 const regexWithValue = validation.pattern.replace( new RegExp(`\\$\\{${paramName}\\}`, 'g'), paramValue ); return { ...validation, pattern: regexWithValue, resolved: true }; } return validation; }); return { ...mw, validations: updatedValidations }; } return mw; }); } evaluateStaticExpression(node, args = []) { if (t.isArrayExpression(node)) { return node.elements.map(elem => this.evaluateStaticExpression(elem, args)); } else if (t.isNumericLiteral(node)) { return node.value; } else if (t.isStringLiteral(node)) { return node.value; } else if (t.isIdentifier(node)) { if (args && args.length > 0) { // 实际需要更复杂的参数映射 return `$${node.name}`; } return node.name; } return null; } extractFunctionParams(funcNode) { return funcNode.params.map(param => ({ name: this.generateCode(param), type: this.inferParamType(param) })); } inferParamType(paramNode) { if (t.isIdentifier(paramNode)) return 'any'; if (t.isObjectPattern(paramNode)) return 'object'; if (t.isArrayPattern(paramNode)) return 'array'; if (t.isRestElement(paramNode)) return 'rest'; return 'unknown'; } extractStringValue(node) { if (t.isStringLiteral(node)) return node.value; return null; } generateCode(node) { try { return generator(node, { concise: true, retainLines: false, sourceMaps: false }).code; } catch (e) { return '/* code generation error */'; } } generateCompleteReport() { const summary = { totalRoutes: this.routes.length, uniquePaths: new Set(this.routes.map(r => r.path)).size, methods: {}, routerTypes: {}, validationCount: 0, middlewareCount: 0, dynamicRoutes: this.routes.filter(r => r.type !== 'static').length, staticRoutes: this.routes.filter(r => r.type === 'static').length }; this.routes.forEach(route => { summary.methods[route.method] = (summary.methods[route.method] || 0) + 1; summary.routerTypes[route.router] = (summary.routerTypes[route.router] || 0) + 1; summary.validationCount += (route.validations?.length || 0); summary.middlewareCount += (route.middleware?.length || 0); }); return { metadata: { analyzedAt: new Date().toISOString(), file: this.currentFile, imports: Array.from(this.imports.values()), functions: Array.from(this.functionDefinitions.keys()), routers: Array.from(this.routerDeclarations.keys()) }, summary, routes: this.routes.map(route => ({ method: route.method, path: route.path, originalDefinition: route.originalPath, router: route.router, type: route.type, parameters: route.parameters, queryParameters: route.queryParameters, middleware: route.middleware.map(mw => ({ name: mw.name || mw.type, type: mw.type, validations: mw.validations || [] })), validations: route.validations || [], responses: route.responses || [], handlers: route.handlers.handlers, location: route.location, file: route.file })), analysis: { routePatterns: this.extractRoutePatterns(), validationPatterns: this.extractValidationPatterns(), commonMiddlewares: this.findCommonMiddlewares(), parameterUsage: this.analyzeParameterUsage() } }; } extractRoutePatterns() { const patterns = new Map(); this.routes.forEach(route => { const pattern = route.path .replace(/:\w+/g, ':param') .replace(/\/\d+/g, '/:id') .replace(/\/[^\/]+\/[^\/]+/g, '/:resource/:id'); patterns.set(pattern, (patterns.get(pattern) || 0) + 1); }); return Array.from(patterns.entries()) .sort((a, b) => b[1] - a[1]) .map(([pattern, count]) => ({ pattern, count })); } extractValidationPatterns() { const patterns = []; this.routes.forEach(route => { route.validations.forEach(validation => { if (validation.type === 'regex') { patterns.push({ pattern: validation.pattern, message: validation.message, routes: [route.path], count: 1 }); } }); }); return patterns; } findCommonMiddlewares() { const middlewareCount = new Map(); this.routes.forEach(route => { route.middleware.forEach(mw => { const key = mw.name || mw.type; middlewareCount.set(key, (middlewareCount.get(key) || 0) + 1); }); }); return Array.from(middlewareCount.entries()) .sort((a, b) => b[1] - a[1]) .map(([name, count]) => ({ name, count, percentage: (count / this.routes.length * 100).toFixed(1) + '%' })); } analyzeParameterUsage() { const paramUsage = new Map(); this.routes.forEach(route => { route.parameters.forEach(param => { const key = `path:${param.name}`; paramUsage.set(key, (paramUsage.get(key) || 0) + 1); }); route.queryParameters.forEach(param => { const key = `query:${param.name}`; paramUsage.set(key, (paramUsage.get(key) || 0) + 1); }); }); return Array.from(paramUsage.entries()) .sort((a, b) => b[1] - a[1]) .map(([param, count]) => { const [type, name] = param.split(':'); return { type, name, count }; }); }}module.exports = AdvancedASTRouteExtractor;if (require.main === module) { const code = `const express = require('express');const router = express.Router();const createRoutes = (versions) => { versions.forEach(ver => { router.get(\`/v\${ver}/:entity\`, validate(ver), (req, res) => { const { entity } = req.params; const ids = req.query.ids?.split(',').map(Number) || []; res.json({ entity, version: ver, ids }); }); });};const validate = (version) => (req, res, next) => { const regex = version >= 2 ? /^[A-Z]\\d{3}\$/ : /^\\w{4,8}\$/; if (!regex.test(req.params.entity)) { return res.status(400).send('实体格式错误'); } next();};createRoutes([1, 2]);router.param('entity', (req, res, next, value) => { req.entity = { raw: value, normalized: value.toLowerCase() }; next();});module.exports = router;`; const extractor = new AdvancedASTRouteExtractor(); const result = extractor.parseCode(code, 'router.js'); console.log('=== AST 路由提取结果 ==='); console.log(JSON.stringify(result, null, 2));}
解析结果如下:
//=== AST 路由提取结果 ==={ "metadata": { "analyzedAt": "2026-01-06T07:12:12.859Z", "file": "router.js", "imports": [ { "type": "require", "source": "express", "name": "express" } ], "functions": [ "createRoutes", "validate" ], "routers": [ "router" ] }, "summary": { "totalRoutes": 1, "uniquePaths": 1, "methods": { "GET": 1 }, "routerTypes": { "router": 1 }, "validationCount": 0, "middlewareCount": 2, "dynamicRoutes": 1, "staticRoutes": 0 }, "routes": [ { "method": "GET", "path": "/v{ver}/:entity", "originalDefinition": "`/v${ver}/:entity`", "router": "router", "type": "dynamic_template", "parameters": [ { "name": "entity", "type": "path", "required": true, "location": "path", "pattern": null } ], "queryParameters": [], "middleware": [ { "name": "call", "type": "call", "validations": [] }, { "name": "inline", "type": "inline", "validations": [] } ], "validations": [], "responses": [ { "type": "success", "method": "json", "data": { "entity": "$entity", "version": "$ver", "ids": "$ids" }, "location": { "start": { "line": 9, "column": 6, "index": 307 }, "end": { "line": 9, "column": 45, "index": 346 } } } ], "handlers": [ { "type": "function", "params": [ { "name": "req", "type": "any" }, { "name": "res", "type": "any" } ], "index": 1, "queryParams": [ "ids" ], "bodyParams": [], "responses": [ { "type": "success", "method": "json", "data": { "entity": "$entity", "version": "$ver", "ids": "$ids" }, "location": { "start": { "line": 9, "column": 6, "index": 307 }, "end": { "line": 9, "column": 45, "index": 346 } } } ], "body": "{ const { entity } = req.params; const ids = req.query.ids?.split(',').map(Number) || []; res.json({ entity, version: ver, ids }); }" } ], "location": { "start": { "line": 6, "column": 4, "index": 139 }, "end": { "line": 10, "column": 6, "index": 354 } }, "file": "router.js" } ], "analysis": { "routePatterns": [ { "pattern": "/:resource/:id", "count": 1 } ], "validationPatterns": [], "commonMiddlewares": [ { "name": "call", "count": 1, "percentage": "100.0%" }, { "name": "inline", "count": 1, "percentage": "100.0%" } ], "parameterUsage": [ { "type": "path", "name": "entity", "count": 1 } ] }}
对比可得,基于语义分析的AST可以获取更多的攻击面,这在安全管理与暴露面收敛都是比基于正则的提取要快且全面的,此篇仅作为思路启发,希望大佬们多指点指点。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:鉴帷安全 鉴帷安全《基于AST的资产路由端点提取新方案》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论