JeecgBoot积木报表AviatorScript注入漏洞深度解析与修复实战 1. 项目概述从一次紧急安全告警说起那天下午我正在调试一个基于JeecgBoot 1.7.8版本开发的内部管理系统突然收到安全扫描平台发来的高危告警直指系统中的“积木报表”模块存在表达式注入风险。告警信息里赫然写着“AviatorScript Injection”这让我心头一紧。JeecgBoot作为国内流行的低代码开发平台其内置的积木报表模块被大量用于快速生成各种统计图表和数据报表如果这里存在注入漏洞意味着所有使用了该模块的报表其背后的数据乃至整个应用服务器都可能暴露在风险之下。这绝不是危言耸听攻击者完全可能通过精心构造的报表参数让AviatorScript引擎执行任意系统命令从而窃取数据、破坏服务。我立刻停下了手头的工作开始深入排查。问题的核心在于AviatorScript这个表达式引擎。它本是一个优秀的工具设计用于安全地执行如userAge 18、totalPrice * discount这类简单的逻辑或运算表达式常被集成在规则引擎、报表计算等场景中。但在积木报表1.7.8的某些实现中由于对用户输入缺乏严格的过滤和沙箱隔离导致本应只用于数据计算的表达式变成了攻击者注入恶意代码的“后门”。这就像你给计算器输入算式它却突然能打开你的文件管理器一样危险。本次排查与修复的目标非常明确第一精准定位JeecgBoot积木报表1.7.8版本中AviatorScript表达式被不当执行的具体代码位置第二深入理解漏洞产生的根本原因与可利用的途径第三提供一套可立即落地、有效且对业务影响最小的修复方案。无论你是该平台的使用者、维护者还是对Java应用安全感兴趣的安全工程师理解这个漏洞的来龙去脉及修复方法都至关重要。接下来我将带你一步步还原我的排查过程并分享最终的修复代码与加固思路。2. 漏洞原理深度解析AviatorScript为何会“失控”要修复漏洞必须先吃透原理。AviatorScript不是一个全功能的脚本语言如JavaScript或Groovy它主要专注于表达式求值。其基本用法是通过AviatorEvaluator.execute(expression, env)来执行其中expression是字符串形式的表达式env是一个存放变量的Map环境。在理想的安全模型中expression应该是开发人员预定义或经过严格校验的而env中的变量是用户可控的数据。危险就潜伏在这个分工的模糊地带。2.1 积木报表中的表达式应用场景在积木报表中AviatorScript表达式被广泛用于实现动态报表逻辑主要集中在以下几个功能点数据集SQL参数动态化在定义数据集时SQL语句的WHERE条件可能包含类似WHERE dept_id ${deptId}的占位符。报表设计者可以在“参数配置”中将deptId这个参数的值设置为一个AviatorScript表达式例如user.getDeptId()以期在报表渲染时动态获取当前用户的部门ID。单元格高级计算在报表设计界面单元格的值可以设置为表达式例如对同一列的数据进行求和sum(C2)或者进行复杂的条件判断。条件显示与格式化控制某行、某列是否显示的规则或者单元格背景色等格式化逻辑也可能通过表达式驱动。问题的关键在于这些表达式的内容从何而来如果报表的设计者通常是内部开发或实施人员是表达式的唯一编写者风险相对可控。但积木报表为了灵活性往往允许将表达式本身或表达式中的关键部分如函数名、参数作为报表参数由前端用户传入。这就打开了潘多拉魔盒。2.2 漏洞产生的核心链用户输入直达执行引擎漏洞产生的典型链条如下入口攻击者通过浏览器访问报表页面在报表参数输入框中并非输入一个普通的数值或字符串而是输入一段恶意的AviatorScript表达式代码。传递这个恶意参数通过HTTP请求被传递到后端控制器。拼接/替换后端代码未经过滤或校验直接将用户输入的字符串拼接进即将执行的AviatorScript表达式字符串中或者直接将其设置为表达式本身。执行拼接后的完整表达式字符串被传递给AviatorEvaluator.getInstance().execute()方法。失控AviatorScript引擎忠实地执行了该字符串由于AviatorScript支持通过java.lang.Runtime等类执行系统命令攻击者便实现了远程代码执行。一个最简单的恶意输入可能是__。这行表达式的含义是通过java.lang.Runtime类的getRuntime()方法获取运行时环境然后执行calc.exe命令在Windows上会弹出计算器。在Linux系统上则可以尝试执行/bin/sh -c等命令。2.3 为什么默认配置不安全许多人有一个误解AviatorScript不是有安全模式吗是的AviatorEvaluator可以通过setOption配置一些安全选项例如禁用某些函数。但在积木报表1.7.8的默认集成方式中往往使用的是无任何安全限制的、功能全开的单例实例。更关键的是即使用户输入被包裹在字符串引号内如${userInput}攻击者也可以通过闭合引号、添加运算符等方式“逃逸”出来构造出有效的恶意表达式。例如如果代码是拼接成dept_id userInput 攻击者输入 system(calc) 11最终形成的表达式就可能被解析为有效的逻辑语句并执行。注意这里提到的system函数是AviatorScript的一个内置函数用于执行系统命令是此类注入漏洞最直接的利用目标。在修复时我们必须考虑彻底禁用或重写此类危险函数。3. 手把手漏洞定位与排查实战理论清晰后我们进入实战排查环节。我的环境是JeecgBoot 1.7.8积木报表模块作为其一部分集成。3.1 第一步全局搜索AviatorScript执行入口在IDE中打开项目使用全局搜索功能查找关键词AviatorEvaluator.execute(import com.googlecode.aviator很快在积木报表的源码包通常类似org.jeecg.modules.jmreport.*中我发现了多个可疑的类。其中一个名为JmReportBaseQuery或AviatorUtil的类具体类名可能因版本略有差异引起了我的注意。这个类通常负责解析报表参数中的表达式并执行。关键排查点查看所有调用AviatorEvaluator.getInstance().execute()或类似方法的地方。重点关注第一个参数即表达式字符串expression它的构成方式。3.2 第二步分析表达式字符串的构造逻辑我找到了核心的代码段简化后如下所示// 模拟漏洞代码片段 public Object evaluateExpression(String expressionStr, MapString, Object env) { // 通常expressionStr可能来自报表配置但env中的某个变量可能直接由用户输入填充 // 危险操作直接将用户可控的字符串拼接进表达式 // 例如来自前端的参数 paramValue 被直接放入env env.put(paramFromUser, request.getParameter(dangerousParam)); // 然后在另一个地方表达式可能被这样构造 String dynamicExpression someLogic( env.get(paramFromUser) ); // 或者更隐蔽的表达式模板来自数据库用户参数直接替换占位符 // String template age #{value}; // 从数据库读取 // String finalExpression template.replace(#{value}, userInput); return AviatorEvaluator.getInstance().execute(dynamicExpression, env); }在上面的伪代码中如果dangerousParam的值是1) system(calc) (11那么拼接后的dynamicExpression就可能变成someLogic(1) system(calc) (11)从而在someLogic函数执行后继续执行system(calc)。实操心得不要只盯着明显的execute调用看。要逆向追踪传入execute方法的那个expressionStr字符串看它是否由StringBuilder拼接、是否经过replace处理、其部分子串是否来源于HttpServletRequest.getParameter()、JSONObject.getString()等直接从外部获取数据的方法。3.3 第三步确定攻击面与验证漏洞根据代码分析我梳理出几个最有可能的攻击面数据集参数值在报表预览时传入的查询参数值如果被解析为表达式。报表SQL中的表达式占位符在SQL语句中使用${...}包裹的表达式。单元格值表达式虽然较少由用户直接输入但如果设计不当也可能存在风险。为了验证我搭建了一个简单的测试报表。在报表中定义一个参数testParam并在数据集的SQL中引用它SELECT * FROM sys_user WHERE username ${testParam}。理论上${testParam}应该被替换为一个具体的值。然后我在报表预览界面不输入普通的用户名而是尝试输入一个简单的AviatorScript表达式例如admin admin。如果后端直接将其作为表达式求值返回true那么SQL可能会变成WHERE username true这显然不正常但也证明了表达式被执行。更进一步可以尝试输入11 ? ‘admin‘ : ‘guest‘这类三元表达式观察返回的数据是否变成了admin用户的数据。请注意在授权的测试环境中进行切勿在生产环境尝试任何可能造成破坏的payload。重要警告漏洞验证环节务必在隔离的测试环境进行。切勿使用真实的、危害性的命令执行payload如system(‘rm -rf /‘)进行测试这可能导致测试环境瘫痪。使用无害的验证方式如计算一个表达式12或者访问一个不存在的Java类来触发异常从而观察执行行为。4. 多层次修复方案设计与实现找到漏洞根源后修复思路必须立体化不能只打一个补丁。我设计了从“紧急止血”到“长治久安”的三层修复方案。4.1 第一层代码层修复——严格过滤与安全调用这是最直接、最快速的修复方式。修改执行AviatorScript表达式的核心工具类。修复目标确保任何来自用户输入前端参数、数据库存储的模板变量部分的内容在进入AviatorScript引擎前都只被当作数据字符串、数字而绝不能成为表达式逻辑的一部分。方案一白名单变量替换法推荐这是最安全的方法。彻底改变使用方式不拼接表达式而是将用户输入作为环境变量中的值表达式只引用变量名。重构表达式模板将原来SQL中或表达式中的直接拼接点改为变量占位符。例如将“age ” userInput改为“age #{ageParam}”。安全替换在代码中解析出#{ageParam}然后从用户请求参数中获取ageParam的值假设为“25”。关键步骤不进行字符串替换而是将ageParam25这个键值对放入执行环境Map (env) 中。执行执行一个固定的表达式“age ageParam”并将env其中ageParam25传入。这样用户输入的“25”始终是一个数值型数据没有任何机会被解析为表达式片段。// 修复后的安全调用示例 public Object evaluateExpressionSafely(String fixedExpression, MapString, Object params) { // fixedExpression 是开发人员预定义的如 age ageParam status statusParam // params 来自用户输入但已经过基础类型转换如字符串转数字 MapString, Object env new HashMap(); for (Map.EntryString, Object entry : params.entrySet()) { Object value entry.getValue(); // 进行基础的数据清洗和类型转换确保value是简单类型String, Number, Boolean // 绝对禁止value是一个包含AviatorScript代码的字符串 env.put(entry.getKey(), convertToSimpleType(value)); } // 使用一个经过安全配置的AviatorEvaluator实例 return getSecureAviatorEvaluator().execute(fixedExpression, env); } private AviatorEvaluatorInstance getSecureAviatorEvaluator() { // 创建独立的评估器实例而非使用全局单例 AviatorEvaluatorInstance instance AviatorEvaluator.newInstance(); // 禁用system等危险函数 instance.disableFunction(system); instance.disableFunction(exec); instance.disableFunction(load); // 可选设置运行模式为安全模式限制访问类 // instance.setOption(Options.FEATURE_SET, Feature.asSet(Feature.Assignment, Feature...)); // 仅开启必要特性 return instance; }方案二黑名单过滤与转义应急方案如果无法立即重构所有表达式逻辑可以采取临时加固措施。对即将拼接进表达式的用户输入进行严格的过滤。过滤所有危险字符如反引号、分号、括号、以及java.lang、Runtime、ProcessBuilder、system、exec等关键词。但这种方法很容易被绕过如大小写变形、编码、字符串拼接因此只能作为临时补充。强制字符串化确保用户输入在表达式中始终被当作字符串字面量。例如在拼接时无论用户输入什么都为其加上单引号并对输入中的单引号进行转义。但这种方式在复杂表达式构造中极易出错。实操心得白名单变量替换法是治本之策。它彻底切断了用户输入“升级”为代码逻辑的路径。实施时需要梳理所有报表的表达式使用点工作量可能不小但安全收益是永久的。对于存量报表可以分批改造对于新报表必须强制使用新规范。4.2 第二层配置层加固——定制安全的AviatorEvaluator实例不要使用全局的AviatorEvaluator.getInstance()。应该为积木报表模块创建一个独立的、经过严格安全配置的实例。禁用危险函数通过AviatorEvaluatorInstance.disableFunction(“system”)等方法禁用命令执行、文件访问、类加载等所有高危函数。限制可访问的Java类通过自定义FunctionLoader和覆盖getProperty方法可以严格控制表达式能够访问的Java类和方法。例如只允许访问特定的工具类、数据模型类禁止访问java.lang.Runtime、java.lang.ProcessBuilder、java.io.File等。设置求值超时通过AviatorEvaluatorInstance.setOption(Options.MAX_LOOP_COUNT, 100)或超时机制防止恶意表达式陷入死循环消耗资源。Configuration public class AviatorSecurityConfig { Bean(name reportAviatorEvaluator) public AviatorEvaluatorInstance secureAviatorEvaluator() { AviatorEvaluatorInstance instance AviatorEvaluator.newInstance(); // 1. 禁用危险内置函数 String[] dangerousFunctions {system, exec, load, read, write, delete}; for (String func : dangerousFunctions) { try { instance.disableFunction(func); } catch (Exception e) { // 忽略函数不存在的异常 } } // 2. 设置安全模式选项限制能力集 EnumSetFeature safeFeatures EnumSet.of( Feature.Assignment, // 允许赋值 Feature.Lambda, // 允许lambda根据需求 Feature.If, // 允许三元表达式 Feature.Loop, // 允许循环可酌情关闭 Feature.Let // 允许let // 谨慎开启Feature.New Feature.InstanceOf 等 ); instance.setOption(Options.FEATURE_SET, safeFeatures); // 3. 设置求值超时单位毫秒 instance.setOption(Options.EVAL_TIMEOUT, 5000L); // 5秒超时 return instance; } }然后在报表服务中注入这个reportAviatorEvaluatorBean来使用。4.3 第三层架构层建议——输入校验与权限最小化强化参数校验在报表参数入口处Controller层使用JSR 303注解或自定义校验器对参数进行强类型和范围校验。例如如果参数应该是部门ID则校验其必须为整数且在有效ID列表内。表达式签名或审核对于需要高安全级别的场景可以考虑对报表中使用的表达式模板进行“签名”或“审核”流程。只有经过预审、签名的表达式模板才允许被执行。动态从用户端传来的表达式字符串应被直接拒绝。权限隔离运行报表服务的应用服务器账户应遵循权限最小化原则避免使用root或高权限账户运行。这样即使发生命令执行其破坏力也受到限制。5. 修复步骤实操与代码示例假设我们定位到的漏洞核心类为JmReportExpressionEngine。以下展示具体的修复步骤。5.1 步骤一创建安全配置类首先创建上述AviatorSecurityConfig配置类定义安全的AviatorEvaluator实例。5.2 步骤二改造表达式引擎类找到原来的JmReportExpressionEngine类名可能不同注入安全实例并重写表达式求值方法。Service public class JmReportExpressionEngine { Autowired Qualifier(reportAviatorEvaluator) // 注入我们配置的安全实例 private AviatorEvaluatorInstance aviatorEvaluator; /** * 安全的表达式求值方法 * param expression 固定的表达式模板如 “param1 0 param2 ‘active‘” * param paramMap 用户参数Map其中的值只能是简单类型String, Number, Boolean, Collection等 * return 求值结果 */ public Object evaluateSafe(String expression, MapString, Object paramMap) { if (StringUtils.isBlank(expression)) { return null; } // 深度清洗参数确保paramMap中的值没有嵌套Map/List包含危险内容可根据需要递归检查 MapString, Object safeEnv sanitizeParameters(paramMap); try { // 设置一个线程局部的超时钩子双重保障 FutureObject future Executors.newSingleThreadExecutor().submit(() - aviatorEvaluator.execute(expression, safeEnv) ); return future.get(5, TimeUnit.SECONDS); // 获取结果超时则中断 } catch (TimeoutException e) { throw new RuntimeException(表达式求值超时可能存在恶意循环: expression, e); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(表达式求值失败: expression, e); } } /** * 参数清洗将用户输入转换为安全的数据 */ private MapString, Object sanitizeParameters(MapString, Object rawParams) { MapString, Object safeParams new HashMap(); for (Map.EntryString, Object entry : rawParams.entrySet()) { Object value entry.getValue(); // 只允许基本类型、字符串、以及简单集合集合元素也需为简单类型 if (isSimpleType(value)) { safeParams.put(entry.getKey(), value); } else { // 对于复杂对象可以尝试只取其安全属性或者直接记录日志并忽略/抛出异常 log.warn(发现非简单类型参数已忽略。key: {}, type: {}, entry.getKey(), value.getClass()); // 安全策略可以抛异常或者转换为字符串需警惕toString方法的风险 // safeParams.put(entry.getKey(), String.valueOf(value)); } } return safeParams; } private boolean isSimpleType(Object obj) { return obj null || obj instanceof String || obj instanceof Number || obj instanceof Boolean || obj instanceof Character || obj instanceof Collection || // 集合需要递归检查元素 obj instanceof Map; // Map需要递归检查key和value // 注意对Collection和Map需要递归检查其内部元素是否为简单类型这里为简化示例 } }5.3 步骤三修改报表数据查询服务找到原来执行SQL拼接和表达式替换的Service类例如JmReportBaseQuery或DatasetService。修改前危险代码示例String sqlTemplate “SELECT * FROM order WHERE amount #{value}”; String userInput params.get(“value”); // 用户输入可能是 “100) OR (11” // 危险直接字符串替换 String finalSql sqlTemplate.replace(“#{value}”, userInput); // 然后执行 finalSql...修改后安全代码示例// 假设SQL模板解析后识别出表达式参数为 #{expr:filterCondition} String sqlTemplate “SELECT * FROM order WHERE #{expr:filterCondition}”; String expressionTemplate “amount amountParam”; // 从报表设计元数据中读取是固定的 MapString, Object env new HashMap(); env.put(“amountParam”, params.get(“amount”)); // params.get(“amount”) 应该是清洗过的数字 // 使用安全的表达式引擎求值 Object conditionResult expressionEngine.evaluateSafe(expressionTemplate, env); // conditionResult 应该是布尔值 true/false // 但这里逻辑需要调整我们不应该将表达式结果拼回SQL而是应该用表达式结果来动态生成SQL的WHERE部分 // 更安全的做法是使用预编译SQLPreparedStatement和参数化查询 String safeSql “SELECT * FROM order”; ListObject queryParams new ArrayList(); if (Boolean.TRUE.equals(conditionResult)) { safeSql “ WHERE amount ?”; queryParams.add(params.get(“amount”)); // 使用预编译参数 } // 然后使用 jdbcTemplate 或 MyBatis 执行 safeSql 和 queryParams关键转变从“将用户数据拼接到表达式或SQL中”转变为“将用户数据作为参数传递给一个固定的、安全的表达式或预编译SQL”。这个思维转变是修复的核心。6. 回归测试与验证要点修复完成后必须进行全面的回归测试。功能测试测试所有现有报表确保正常参数下报表能正确生成数据准确。测试表达式功能是否按预期工作如条件显示、动态计算。安全测试输入验证在报表参数中输入各种边界值和异常值空值、超长字符串、特殊字符。注入尝试输入之前发现的漏洞Payload例如包含system、Runtime、反引号、括号的字符串。预期结果应该是报表渲染失败并返回一个清晰的、非泄露性的错误信息如“参数格式错误”绝对不允许执行Payload。模糊测试使用工具或脚本随机生成大量非常规输入观察系统行为是否稳定有无错误信息泄露、服务崩溃等情况。性能测试由于引入了额外的参数清洗和安全检查以及可能使用了独立的AviatorEvaluator实例需关注对报表加载性能的影响。在高并发下进行压力测试确保响应时间在可接受范围内。日志与监控在表达式引擎的关键位置添加详细的日志记录注意不要记录敏感参数。监控日志中是否有频繁的表达式执行失败或安全警告这可能是攻击试探的迹象。7. 总结与长效安全治理建议通过这次对JeecgBoot积木报表1.7.8 AviatorScript注入漏洞的深度排查与修复我们不仅解决了一个具体的安全问题更梳理出了一套针对表达式引擎使用的安全最佳实践。核心要点再强调一次永远不要信任用户输入永远不要将用户输入直接作为代码逻辑的一部分。对于使用JeecgBoot或类似集成AviatorScript、OGNL、SpEL等表达式引擎的框架我建议将安全检查纳入开发流程代码审计定期对使用表达式引擎的代码进行人工或工具辅助的审计重点关注字符串拼接、replace、format等方法。依赖管理及时升级框架和组件。关注JeecgBoot官方仓库的Security Advisories看是否有针对此漏洞的官方补丁发布。安全编码规范在团队内建立规范明确要求所有动态表达式必须使用“白名单变量替换”模式禁止任何形式的字符串拼接。运行时防护考虑在WAFWeb应用防火墙或RASP运行时应用自保护层面增加对特定漏洞特征如Runtime.getRuntime()的检测和阻断作为最后一道防线。修复这个漏洞的过程就像给一扇原本虚掩的门加了一把结实的锁。门的功能报表灵活性没有丢失但未经授权的访问被彻底杜绝。安全是一个持续的过程保持警惕遵循最小权限和零信任原则才能让我们的应用在享受便利的同时抵御住外部的风雨。