FreeMarker 安全控制与沙箱模式 全面指南
版本兼容性说明:基于 FreeMarker 2.3.x(主流稳定版本),适用于 Java 8+ 环境。
安全目标:防止模板执行任意 Java 代码、访问敏感资源、调用危险方法。
一、核心概念
1. 为什么需要安全控制?
FreeMarker 模板默认允许访问 Java 对象的 getter 方法、属性、静态变量、构造函数,如果不加限制,攻击者可能通过模板注入执行恶意操作,例如:
${object?api.system("rm -rf /")} <!-- 危险! -->
${object?api.getClass().forName("java.lang.Runtime")}
2. 沙箱模式(Sandbox)是什么?
FreeMarker 的“沙箱”并非独立模式,而是通过 配置限制模板能力 来实现安全隔离,主要包括:
- 禁用对 Java API 的直接访问(
?api
) - 限制可调用的类和方法
- 禁用危险内置函数(如
new
、method
) - 使用白名单机制控制暴露的对象
3. 核心安全机制
机制 | 作用 |
---|---|
object_wrapper |
控制模板如何访问 Java 对象(关键!) |
?api 内置指令 |
访问底层 Java API,默认禁用更安全 |
security_manager |
JVM 级别安全管理(不推荐用于 FreeMarker) |
白名单类/方法 | 显式允许某些安全类在模板中使用 |
二、操作步骤(非常详细)
步骤 1:引入依赖(Maven)
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.32</version>
</dependency>
步骤 2:创建安全的 Configuration
实例
import freemarker.template.Configuration;
import freemarker.template.DefaultObjectWrapper;
import freemarker.template.TemplateExceptionHandler;
Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
步骤 3:配置安全的对象包装器(ObjectWrapper)——关键步骤!
✅ 推荐方式:使用 SimpleObjectWrapper
或限制性 DefaultObjectWrapper
// 方式一:使用 SimpleObjectWrapper(最安全)
import freemarker.template.SimpleObjectWrapper;
cfg.setObjectWrapper(SimpleObjectWrapper.getInstance());
✅ 优点:仅暴露基本类型、集合、Map、String,不支持
?api
,无法调用任意 Java 方法。
// 方式二:使用 DefaultObjectWrapper 并禁用 ?api
import freemarker.template.DefaultObjectWrapperBuilder;
DefaultObjectWrapperBuilder builder = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_32);
DefaultObjectWrapper wrapper = builder.build();
wrapper.setExposeFields(false); // 不暴露字段
wrapper.setMethodVisibilityLevel(
freemarker.template.TemplateModel.EXPOSE_NOTHING); // 不暴露任何方法
cfg.setObjectWrapper(wrapper);
⚠️
?api
在DefaultObjectWrapper
下默认启用,必须显式关闭或避免使用。
步骤 4:禁用危险内置功能
// 禁用 new() 构造函数调用
cfg.setNewBuiltinClassResolver(
new freemarker.core.SecurityUtilities.NullClassResolver()
);
// 禁用 method() 内置函数(调用任意方法)
cfg.setSharedVariable("method", null); // 移除 method 内置函数
💡
method("java.lang.System", "exit")
这类调用将失效。
步骤 5:设置白名单类(可选但推荐)
只允许模板访问你明确授权的安全类:
// 示例:允许使用 java.util.Date,但不暴露其方法
cfg.setSharedVariable("Date", new TemplateMethodModelEx() {
public Object exec(List args) throws TemplateModelException {
return new Date();
}
});
或通过自定义 TemplateHashModel
提供安全的工具类:
public class SafeTools implements TemplateHashModel {
public TemplateModel get(String key) {
if ("now".equals(key)) {
return new TemplateDateModel(new Date(), new SimpleDateFormat("yyyy-MM-dd"));
}
return null;
}
}
cfg.setSharedVariable("safe", new SafeTools());
模板中使用:${safe.now}
步骤 6:配置其他安全选项
// 编码
cfg.setDefaultEncoding("UTF-8");
// 错误处理:不要暴露堆栈信息给前端
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.HTML_DEBUG_HANDLER); // 或 REHTHROW
// 日志
cfg.setLogTemplateExceptions(false);
cfg.setWrapUncheckedExceptions(true);
// 禁止模板中包含敏感路径
cfg.setAllowedTemplatePaths(Arrays.asList("/templates/")); // FreeMarker 2.3.31+ 支持
// 模板更新延迟(生产环境关闭检查)
cfg.setTemplateUpdateDelayMilliseconds(Long.MAX_VALUE);
步骤 7:准备数据模型(避免暴露危险对象)
Map<String, Object> dataModel = new HashMap<>();
// ❌ 危险:暴露原始 Java 对象
// dataModel.put("system", System.class);
// ✅ 安全:只暴露必要数据
dataModel.put("user", "Alice");
dataModel.put("items", Arrays.asList("A", "B"));
// ✅ 可暴露安全包装对象
dataModel.put("utils", new SafeStringUtils());
步骤 8:渲染模板(自动受安全配置保护)
try {
Template template = cfg.getTemplate("user.ftl");
StringWriter out = new StringWriter();
template.process(dataModel, out);
System.out.println(out.toString());
} catch (Exception e) {
// 安全处理异常
throw new RuntimeException("模板渲染失败", e);
}
三、常见错误与解决方案
错误现象 | 原因 | 解决方案 |
---|---|---|
The ?api method is not allowed |
?api 被禁用 |
正常现象,说明安全生效;如需使用,应评估风险 |
Method not visible |
方法不可见 | 检查 setMethodVisibilityLevel 设置 |
Access denied to class java.lang.Runtime |
尝试调用危险类 | 确保未通过 sharedVariable 暴露 |
Template not found |
路径问题 | 检查 setAllowedTemplatePaths 是否限制过严 |
ClassCastException in wrapper |
包装器不兼容 | 使用 SimpleObjectWrapper 或统一版本 |
四、注意事项
- 永远不要在模板中暴露
?api
给不可信用户(如用户自定义模板场景)。 - 避免将
System
、Runtime
、Class
、ClassLoader
等放入数据模型。 - 不要使用
BeansWrapper
默认配置,它会暴露大量 Java API。 - 生产环境应关闭
template_update_delay
,防止频繁 IO 检查。 - 日志中不要打印模板内容,防止敏感信息泄露。
- 用户上传的模板必须经过审核或沙箱隔离。
五、使用技巧
1. 动态禁用 ?api
(通过配置)
// 在 wrapper 中禁用
DefaultObjectWrapper wrapper = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_32)
.build();
wrapper.setAPIBuiltinEnabled(false); // 关键!禁用 ?api
cfg.setObjectWrapper(wrapper);
2. 自定义安全包装器
public class SecureObjectWrapper extends DefaultObjectWrapper {
@Override
protected TemplateModel handleUnknownType(Object obj) throws TemplateModelException {
if (obj instanceof String || obj instanceof Number || obj instanceof Boolean) {
return super.handleUnknownType(obj);
}
// 其他类型返回 null 或安全包装
return new SimpleScalar("<secure>");
}
}
3. 模板内容过滤(防 XSS)
// 在模板中使用 ?html 内置函数
${userInput?html}
或在 Java 层预处理:
String escaped = StringEscapeUtils.escapeHtml4(input);
dataModel.put("safeInput", escaped);
六、最佳实践
实践 | 说明 |
---|---|
✅ 使用 SimpleObjectWrapper |
最安全,适合大多数场景 |
✅ 禁用 ?api 和 new() |
关键安全措施 |
✅ 白名单共享变量 | 只暴露必要工具 |
✅ 模板路径限制 | 防止路径遍历 |
✅ 数据模型最小化 | 不暴露原始 Java 类 |
✅ 异常处理脱敏 | 不返回堆栈到前端 |
✅ 定期审计模板 | 检查是否有危险语法 |
七、性能优化(安全与性能兼顾)
优化项 | 说明 |
---|---|
启用模板缓存 | Configuration 默认启用,提升性能 |
缓存 Template 实例 | 避免重复解析 |
关闭开发期热更新 | 生产环境 template_update_delay = Long.MAX_VALUE |
避免复杂安全逻辑在 wrapper 中 | 影响渲染速度 |
使用 StringTemplateLoader 动态模板 | 减少 IO,但需确保内容安全 |
八、高级安全场景
场景:用户可编辑模板(CMS、邮件模板)
建议方案:
- 使用
SimpleObjectWrapper
- 禁用
?api
、new
、method
- 提供有限的
sharedVariables
(如date
、math
工具) - 模板保存前进行语法扫描(正则匹配
?api
、new(
等) - 运行在独立沙箱 JVM 或容器中(极端安全需求)
九、总结:安全配置检查清单 ✅
检查项 | 是否完成 |
---|---|
使用 SimpleObjectWrapper 或限制性 DefaultObjectWrapper |
☐ |
禁用 ?api (setAPIBuiltinEnabled(false) ) |
☐ |
禁用 new() 内置函数 |
☐ |
不将 Runtime 、System 等放入数据模型 |
☐ |
设置 template_update_delay 生产环境为最大值 |
☐ |
使用 ?html 等内置函数防 XSS |
☐ |
模板路径限制(可选) | ☐ |
异常不暴露敏感信息 | ☐ |
✅ 一句话总结:
FreeMarker 本身无“开关式沙箱”,但通过 限制 ObjectWrapper
、禁用 ?api
、控制数据模型,可构建强大安全边界,防止模板注入与任意代码执行。