fix(codegen): 加固 Velocity 模板渲染

This commit is contained in:
PIGCLOUD committed 2026-06-04 18:32:16 +08:00
1 parent d3966d64f5
commit a5d3dff3fc
1 file changed
+93 -27
@@ -1,15 +1,18 @@
package com.pig4cloud.pigx.codegen.util;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.tools.generic.DateTool;
import org.apache.velocity.tools.generic.MathTool;
import org.springframework.stereotype.Service;
import java.io.StringWriter;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
/**
@@ -21,6 +24,82 @@ import java.util.Properties;
@Service
public class VelocityKit {
/**
* 模板高危关键字黑名单。
* <p>
* 聚焦反射 / 命令执行原语,正常代码生成模板不会出现这些 token,命中即拒绝渲染(早失败)。 真正的安全边界是引擎层的
* {@link org.apache.velocity.util.introspection.SecureUberspector}, 本黑名单仅作纵深防御。
*/
private static final String[] DANGER_TOKENS = { "getClass", "forName", "getRuntime", "java.lang.Runtime",
"java.lang.System", "ProcessBuilder", "ClassLoader", "java.lang.reflect", "getDeclaredMethod",
"scriptEngine", "javax.script.ScriptEngine", "#evaluate" };
/**
* Velocity 安全沙箱限制访问的包。
*/
private static final String[] RESTRICTED_PACKAGES = { "java.lang.reflect", "org.openjdk.nashorn", "bsh" };
/**
* Velocity 安全沙箱限制访问的类。
*/
private static final String[] RESTRICTED_CLASSES = { "java.lang.Class", "java.lang.ClassLoader",
"java.lang.Compiler", "java.lang.InheritableThreadLocal", "java.lang.Package", "java.lang.Process",
"java.lang.ProcessBuilder", "java.lang.Reflect", "java.lang.Runtime", "java.lang.RuntimePermission",
"java.lang.SecurityManager", "java.lang.System", "java.lang.Thread", "java.lang.ThreadGroup",
"java.lang.ThreadLocal", "java.net.Socket", "javax.management.MBeanServer", "javax.script.ScriptEngine" };
/**
* 安全的 Velocity 引擎,全局初始化一次。
* <p>
* 通过 {@link org.apache.velocity.util.introspection.SecureUberspector} 禁止 {@code getClass()}
* 及对 {@code java.lang.Runtime/System/Thread/ProcessBuilder/Process/ClassLoader/Class} 和
* {@code java.lang.reflect.*} 的方法调用,从根上阻断 Velocity SSTI → 反射 → RCE。
*/
private static final VelocityEngine ENGINE = buildSecureEngine();
private static VelocityEngine buildSecureEngine() {
Properties prop = new Properties();
// classpath 资源加载(保留 render() 按名加载模板的能力)
prop.put(RuntimeConstants.RESOURCE_LOADERS, "class");
prop.put("resource.loader.class.class",
"org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
// 关键:安全 Uberspector,禁用反射 / getClass 访问危险类
prop.put(RuntimeConstants.UBERSPECT_CLASSNAME,
"org.apache.velocity.util.introspection.SecureUberspector");
// 显式保留 Velocity 默认限制,并扩展封禁脚本引擎等二次利用面
prop.put("introspector.restrict.packages", String.join(",", RESTRICTED_PACKAGES));
prop.put("introspector.restrict.classes", String.join(",", RESTRICTED_CLASSES));
VelocityEngine engine = new VelocityEngine();
engine.init(prop);
return engine;
}
/**
* 渲染前的安全校验:拦截包含反射 / 命令执行关键字的模板。
* @param template 模板内容
*/
private static void checkTemplateSecurity(String template) {
if (StrUtil.isBlank(template)) {
return;
}
Assert.isFalse(StrUtil.containsAnyIgnoreCase(template, DANGER_TOKENS), "模板包含非法内容,已被安全策略拦截");
}
/**
* 构建注入工具库的渲染上下文
* @param map 数据模型
* @return VelocityContext
*/
private static VelocityContext buildContext(Map<String, Object> map) {
VelocityContext context = new VelocityContext(map);
// 函数库
context.put("math", new MathTool());
context.put("dateTool", new DateTool());
context.put("dict", new DictTool());
context.put("str", new NamingCaseTool());
return context;
}
/**
* Velocity 模板渲染方法
* @param template 模板
@@ -28,41 +107,28 @@ public class VelocityKit {
* @return 渲染结果
*/
public static String render(String template, Map<String, Object> map) {
// 设置velocity资源加载器
Properties prop = new Properties();
prop.put("resource.loader.file.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
Velocity.init(prop);
VelocityContext context = new VelocityContext(map);
// 函数库,使用 Lambda 表达式简化代码
Optional.of(new MathTool()).ifPresent(mt -> context.put("math", mt));
Optional.of(new DateTool()).ifPresent(dt -> context.put("dateTool", dt));
Optional.of(new DictTool()).ifPresent(dt -> context.put("dict", dt));
Optional.of(new NamingCaseTool()).ifPresent(nct -> context.put("str", nct));
// 渲染模板,使用 Lambda 表达式简化代码
checkTemplateSecurity(template);
VelocityContext context = buildContext(map);
StringWriter sw = new StringWriter();
Optional.ofNullable(Velocity.getTemplate(template, CharsetUtil.UTF_8)).ifPresent(tpl -> tpl.merge(context, sw));
getTemplate(template).merge(context, sw);
return sw.toString();
}
private static Template getTemplate(String template) {
return ENGINE.getTemplate(template, CharsetUtil.UTF_8);
}
/**
* 渲染文本
* @param str
* @param str 模板字符串
* @param dataModel 数据
* @return
* @return 渲染结果
*/
public static String renderStr(String str, Map<String, Object> dataModel) {
// 设置velocity资源加载器
Velocity.init();
checkTemplateSecurity(str);
StringWriter stringWriter = new StringWriter();
VelocityContext context = new VelocityContext(dataModel);
// 函数库
context.put("math", new MathTool());
context.put("dateTool", new DateTool());
context.put("dict", new DictTool());
context.put("str", new NamingCaseTool());
Velocity.evaluate(context, stringWriter, "renderStr", str);
VelocityContext context = buildContext(dataModel);
ENGINE.evaluate(context, stringWriter, "renderStr", str);
return stringWriter.toString();
}