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
  • 限制可调用的类和方法
  • 禁用危险内置函数(如 newmethod
  • 使用白名单机制控制暴露的对象

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);

⚠️ ?apiDefaultObjectWrapper 下默认启用,必须显式关闭或避免使用。


步骤 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 或统一版本

四、注意事项

  1. 永远不要在模板中暴露 ?api 给不可信用户(如用户自定义模板场景)。
  2. 避免将 SystemRuntimeClassClassLoader 等放入数据模型
  3. 不要使用 BeansWrapper 默认配置,它会暴露大量 Java API。
  4. 生产环境应关闭 template_update_delay,防止频繁 IO 检查。
  5. 日志中不要打印模板内容,防止敏感信息泄露。
  6. 用户上传的模板必须经过审核或沙箱隔离

五、使用技巧

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 最安全,适合大多数场景
✅ 禁用 ?apinew() 关键安全措施
✅ 白名单共享变量 只暴露必要工具
✅ 模板路径限制 防止路径遍历
✅ 数据模型最小化 不暴露原始 Java 类
✅ 异常处理脱敏 不返回堆栈到前端
✅ 定期审计模板 检查是否有危险语法

七、性能优化(安全与性能兼顾)

优化项 说明
启用模板缓存 Configuration 默认启用,提升性能
缓存 Template 实例 避免重复解析
关闭开发期热更新 生产环境 template_update_delay = Long.MAX_VALUE
避免复杂安全逻辑在 wrapper 中 影响渲染速度
使用 StringTemplateLoader 动态模板 减少 IO,但需确保内容安全

八、高级安全场景

场景:用户可编辑模板(CMS、邮件模板)

建议方案

  1. 使用 SimpleObjectWrapper
  2. 禁用 ?apinewmethod
  3. 提供有限的 sharedVariables(如 datemath 工具)
  4. 模板保存前进行语法扫描(正则匹配 ?apinew( 等)
  5. 运行在独立沙箱 JVM 或容器中(极端安全需求)

九、总结:安全配置检查清单 ✅

检查项 是否完成
使用 SimpleObjectWrapper 或限制性 DefaultObjectWrapper
禁用 ?apisetAPIBuiltinEnabled(false)
禁用 new() 内置函数
不将 RuntimeSystem 等放入数据模型
设置 template_update_delay 生产环境为最大值
使用 ?html 等内置函数防 XSS
模板路径限制(可选)
异常不暴露敏感信息

一句话总结
FreeMarker 本身无“开关式沙箱”,但通过 限制 ObjectWrapper、禁用 ?api、控制数据模型,可构建强大安全边界,防止模板注入与任意代码执行。