Freemarker 模板注入安全加固:重写 Configuration 与白名单解析器
简介这份资源面向仍在使用老旧 JeecgBoot 版本、无法平滑升级的 Java 开发者提供一种低成本修复 Freemarker 模板注入漏洞的应急方案。其核心做法是直接重写 freemarker 的 Configuration 类在实例化方法中默认加入 this.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER)从而在不改动整体框架的前提下收紧模板解析策略降低远程代码执行风险。压缩包内共 1 个 java 文件即重写后的 Configuration.java包体约 13KB体量轻便便于直接替换或对照修改。目前已有 1339 人学习下载说明该思路在存量项目维护中具有一定参考价值。对于受限于历史版本、升级成本高的团队这份源码可作为快速加固的起点帮助理解 Freemarker 安全配置的落地方式并据此排查项目中同类模板注入隐患。1. 从一次模板注入告警说起这个项目到底在修什么去年帮一个做内容管理系统的团队排查线上问题安全扫描报告里跳出一条 Freemarker 模板注入告警定位到freemarker.template.Configuration的默认配置上。当时项目用的是 2.3.31 版本模板里允许直接调用?new()这类内建函数攻击者只要能把${freemarker.template.utility.Execute?new()(id)}这种字符串塞进模板就能在服务端执行任意命令。这个项目的核心工作就是重写Configuration的初始化逻辑把new内建函数、ObjectConstructor、Execute这些危险入口全部封死同时保证原有模板渲染功能不受影响。它适合正在用 Freemarker 做页面渲染、又需要过安全合规检查的 Java 后端开发者尤其是那些模板内容部分来自用户输入或数据库的场景。2. 漏洞根因拆解为什么默认 Configuration 会成为注入入口2.1 Freemarker 模板注入的触发链路Freemarker 的模板语言本身支持一套内建函数其中?new()允许在模板里实例化任意实现了TemplateModel的类。这个设计初衷是为了让模板能调用一些工具类但问题在于它没有对类名做白名单限制。当模板内容可控时攻击者可以构造出调用链最终触达freemarker.template.utility.Execute或ObjectConstructor前者直接执行系统命令后者能反射创建任意对象。触发链路大致是这样的用户输入被拼进模板字符串 → 模板引擎解析时遇到?new()→ 通过ClassUtil.forName加载指定类 → 实例化并调用其exec方法。整条链路上Configuration的newBuiltinClassResolver属性决定了哪些类允许被?new()访问默认值是TemplateClassResolver.UNRESTRICTED_RESOLVER也就是不限制。// Freemarker 2.3.31 中 Configuration 的默认行为简化示意 public class Configuration { private TemplateClassResolver newBuiltinClassResolver TemplateClassResolver.UNRESTRICTED_RESOLVER; // 危险默认值 public void setNewBuiltinClassResolver(TemplateClassResolver resolver) { this.newBuiltinClassResolver resolver; } }这段代码说明默认配置下?new()可以加载任意类。参数newBuiltinClassResolver是控制入口只要把它替换成TemplateClassResolver.SAFER_RESOLVER或自定义解析器就能阻断大部分攻击。但很多项目在初始化Configuration时只设置了模板加载路径和编码根本没碰这个属性漏洞就这么留下来了。2.2 为什么直接升级版本不一定能解决Freemarker 后续版本确实收紧了默认策略但实际项目里升级模板引擎版本往往牵一发动全身。模板语法兼容性、自定义指令、宏定义的行为都可能变化尤其是那些模板里已经用了?new()做合法工具调用的老系统升级后直接报错。所以这个项目选择重写Configuration在 2.3.31 的基础上做安全加固而不是简单升级。重写的思路是继承Configuration类在构造阶段就把newBuiltinClassResolver设成白名单模式同时覆盖setNewBuiltinClassResolver方法防止后续代码把它改回不安全的值。这样既保留了原有 API 兼容性又把安全策略固化下来。public class SafeConfiguration extends Configuration { // 白名单只允许模板访问这些工具类 private static final SetString ALLOWED_CLASSES new HashSet(Arrays.asList( freemarker.template.utility.DateUtil, freemarker.template.utility.StringUtil )); public SafeConfiguration() { super(); // 构造时立即锁定安全解析器 super.setNewBuiltinClassResolver(new WhitelistClassResolver()); } Override public void setNewBuiltinClassResolver(TemplateClassResolver resolver) { // 禁止外部覆盖保持白名单策略 throw new UnsupportedOperationException( newBuiltinClassResolver is locked for security); } }这里的关键点是构造器里用super.setNewBuiltinClassResolver绕过自己的重写方法完成初始化后再禁止外部修改。ALLOWED_CLASSES集合就是白名单只有明确列出的类才允许被?new()实例化。实际项目中这个白名单要根据模板里真实用到的工具类来定不能照抄。2.3 白名单解析器的实现细节TemplateClassResolver是一个接口只有一个方法resolve返回允许加载的Class对象。自定义解析器要实现这个方法在里面做白名单校验。public class WhitelistClassResolver implements TemplateClassResolver { Override public Class? resolve(String className, Environment env, Template template) throws TemplateException { if (ALLOWED_CLASSES.contains(className)) { return ClassUtil.forName(className); } // 不在白名单内抛出异常并记录日志 throw new TemplateException( Class className is not allowed in template, env); } }resolve方法的三个参数分别是类名、当前环境对象和模板对象。类名来自模板里的?new()调用环境对象可以用来获取配置信息模板对象能拿到模板名称方便定位问题。校验逻辑很简单在白名单里就放行否则抛异常。异常信息里带上类名方便排查是哪个模板触发了拦截。注意白名单校验要区分大小写并且不能只做前缀匹配。曾经见过有人用startsWith(freemarker.template.utility)做校验结果攻击者用freemarker.template.utility.Execute照样能过因为Execute也在这个包下。3. 工程落地从目录结构到运行验证的完整流程3.1 项目目录结构与模块划分这个项目的代码结构比较紧凑核心就三个包config放安全配置类resolver放白名单解析器test放验证用例。典型的 Maven 项目布局如下freemarker-security/ ├── pom.xml ├── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── example/ │ │ └── freemarker/ │ │ ├── config/ │ │ │ └── SafeConfiguration.java │ │ └── resolver/ │ │ └── WhitelistClassResolver.java │ └── test/ │ └── java/ │ └── com/ │ └── example/ │ └── freemarker/ │ └── SecurityTest.javapom.xml里只需要引入 Freemarker 2.3.31 和 JUnit不需要额外依赖。SafeConfiguration是核心类WhitelistClassResolver是配套的解析器测试类用来验证拦截效果。dependencies dependency groupIdorg.freemarker/groupId artifactIdfreemarker/artifactId version2.3.31/version /dependency dependency groupIdjunit/groupId artifactIdjunit/artifactId version4.13.2/version scopetest/scope /dependency /dependencies依赖版本要锁死避免构建时拉到其他版本导致行为不一致。JUnit 只用于测试生产环境不需要。3.2 安全配置类的初始化与参数说明SafeConfiguration除了锁定解析器还需要设置模板加载路径、编码、异常处理等常规参数。这些参数在构造器里统一初始化避免调用方遗漏。public SafeConfiguration(String templatePath) throws IOException { super(); // 设置模板加载目录 setDirectoryForTemplateLoading(new File(templatePath)); // 统一编码防止乱码导致的解析异常 setDefaultEncoding(UTF-8); // 模板异常时抛出不静默失败 setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); // 锁定安全解析器 super.setNewBuiltinClassResolver(new WhitelistClassResolver()); // 禁用对象包装器的危险方法 setObjectWrapper(new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_31) .build()); }setDirectoryForTemplateLoading指定模板文件所在目录setDefaultEncoding统一用 UTF-8setTemplateExceptionHandler设为RETHROW_HANDLER让异常直接抛出方便定位问题。setObjectWrapper用DefaultObjectWrapperBuilder构建避免使用DefaultObjectWrapper的旧构造方式后者在某些版本里会暴露不安全的反射入口。参数templatePath是模板目录的绝对路径或相对路径建议用绝对路径避免工作目录变化导致找不到模板。VERSION_2_3_31是 Freemarker 的版本常量用来保证对象包装器的行为与引擎版本匹配。3.3 运行验证用攻击载荷测试拦截效果写完配置类后需要写测试用例验证拦截是否生效。测试用例分两组一组是正常模板渲染确保功能没被误伤另一组是攻击载荷确保被正确拦截。Test public void testNormalTemplate() throws Exception { SafeConfiguration cfg new SafeConfiguration(src/test/resources/templates); Template template new Template(hello, Hello ${name}!, cfg); StringWriter writer new StringWriter(); template.process(Collections.singletonMap(name, World), writer); assertEquals(Hello World!, writer.toString()); } Test(expected TemplateException.class) public void testExecuteInjection() throws Exception { SafeConfiguration cfg new SafeConfiguration(src/test/resources/templates); String payload ${freemarker.template.utility.Execute?new()(id)}; Template template new Template(attack, payload, cfg); template.process(new HashMap(), new StringWriter()); }第一个用例渲染一个普通模板验证变量替换正常。第二个用例构造Execute注入载荷期望抛出TemplateException。如果第二个用例没有抛异常说明白名单没生效需要检查WhitelistClassResolver的resolve方法是否被正确调用。运行测试用mvn test即可。如果测试通过说明安全加固生效如果失败看异常堆栈里是哪个类被加载了再调整白名单或解析器逻辑。提示测试载荷里的id只是示例命令实际验证时可以用echo test这类无害命令避免在测试环境执行敏感操作。4. 避坑与排查模板注入加固中的常见问题4.1 白名单遗漏导致正常模板报错现象加固后部分模板渲染失败日志里出现Class xxx is not allowed in template。原因白名单只列了部分工具类模板里用到的其他合法类被拦截。解决先在全量模板里搜索?new()调用把所有用到的类名收集起来逐个确认安全性后加入白名单。不要图省事用包名前缀放行那等于没加固。4.2 子类重写方法被绕过现象明明继承了SafeConfiguration但攻击载荷还是能执行。原因调用方通过反射直接修改了父类的newBuiltinClassResolver字段或者用了Configuration的其他 setter 方法间接改回不安全值。解决在SafeConfiguration里把所有可能影响解析器的 setter 都重写并抛异常同时用final修饰关键字段。如果调用方坚持要改说明它不该用这个安全配置类。4.3 模板缓存导致旧配置生效现象改了配置类重新部署攻击载荷仍然能过。原因Freemarker 的Configuration实例被缓存了或者模板本身被缓存新配置没应用到已缓存的模板上。解决在应用启动时重新创建Configuration实例并调用clearTemplateCache()清空模板缓存。如果是 Spring 项目检查Configuration的 Bean 作用域确保不是单例被其他模块提前初始化。4.4 对象包装器暴露反射入口现象白名单解析器生效了但通过${obj.getClass().forName(...)}这类方式仍然能触达危险类。原因DefaultObjectWrapper默认允许模板访问对象的getClass()方法进而拿到Class对象做反射。解决在SafeConfiguration里设置setObjectWrapper时用DefaultObjectWrapperBuilder并调用setExposeFields(false)和setExposeProperties(false)或者自定义ObjectWrapper限制可访问的方法。4.5 日志泄露敏感信息现象拦截异常里打印了完整的类名和模板内容日志被攻击者利用。原因TemplateException的默认消息包含模板片段和类名。解决在WhitelistClassResolver里捕获异常后只记录类名和模板名称不记录模板内容。生产环境的日志级别调到 WARN 以上避免异常堆栈被外部看到。5. 二次开发与改造把安全配置做成可复用的 Starter5.1 从单项目配置到通用 Starter如果团队里有多个 Java 项目都用 Freemarker把SafeConfiguration做成 Spring Boot Starter 是最省事的做法。核心思路是提供一个Configuration类自动装配SafeConfigurationBean并通过application.yml暴露白名单配置项。Configuration ConditionalOnClass(Configuration.class) public class FreemarkerSecurityAutoConfiguration { Bean ConditionalOnMissingBean public SafeConfiguration safeConfiguration( FreemarkerSecurityProperties properties) throws IOException { SafeConfiguration cfg new SafeConfiguration(properties.getTemplatePath()); // 从配置文件加载额外白名单 properties.getAdditionalWhitelist().forEach( WhitelistClassResolver::addAllowedClass); return cfg; } }FreemarkerSecurityProperties用ConfigurationProperties绑定freemarker.security前缀的配置additionalWhitelist允许各项目按需追加白名单类。ConditionalOnMissingBean保证业务方可以覆盖默认配置ConditionalOnClass保证没有 Freemarker 依赖时自动跳过。5.2 白名单的动态管理硬编码白名单在项目初期够用但模板多了之后维护成本高。可以把白名单放到数据库或配置中心启动时加载运行时支持刷新。改造点是WhitelistClassResolver的ALLOWED_CLASSES从静态集合改成可刷新的ConcurrentHashMap并暴露addAllowedClass和removeAllowedClass方法。public class WhitelistClassResolver implements TemplateClassResolver { private final SetString allowedClasses ConcurrentHashMap.newKeySet(); public void addAllowedClass(String className) { allowedClasses.add(className); } public void removeAllowedClass(String className) { allowedClasses.remove(className); } Override public Class? resolve(String className, Environment env, Template template) throws TemplateException { if (allowedClasses.contains(className)) { return ClassUtil.forName(className); } throw new TemplateException(Blocked: className, env); } }用ConcurrentHashMap.newKeySet()保证并发安全addAllowedClass和removeAllowedClass可以在配置刷新时调用。注意刷新后要清空模板缓存否则已缓存的模板仍然用旧白名单。5.3 验证方法与回归测试改造完成后回归测试要覆盖三类场景正常模板渲染、白名单内类调用、白名单外类拦截。建议把攻击载荷做成参数化测试每次新增白名单类时都跑一遍确保没有引入新的绕过路径。测试场景输入预期结果普通变量替换${name}正常输出白名单类调用${freemarker.template.utility.DateUtil?new()}正常实例化危险类拦截${freemarker.template.utility.Execute?new()}抛 TemplateException反射绕过${obj.getClass().forName(...)}抛异常或返回受限对象这张表可以直接转成 JUnit 参数化测试的用例集。每次改白名单或解析器逻辑先跑这张表全绿了再上预发环境。从那以后我每次给 Freemarker 做安全加固都强制走一遍「收集?new()调用 → 确认白名单 → 跑攻击载荷 → 清缓存重启」的流程少一步都可能留口子。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

H264/H265码流类型详解:Annex-B与AVCC格式原理、转换及避坑指南

H264/H265码流类型详解:Annex-B与AVCC格式原理、转换及避坑指南

1. 码流类型到底在聊什么刚接触音视频编解码这块的朋友,看到“H264/H265码流类型”这个词,第一反应往往是懵的。码流不就是视频数据吗,怎么还有类型之分?我当初也是这么想的,直到有一次在项目里做流媒体转发&#xff0…

2026/10/15 23:37:54 阅读更多 →
Formisch Methods API参考手册:23个表单操控方法一文全解

Formisch Methods API参考手册:23个表单操控方法一文全解

【免费下载链接】formisch The lightweight, schema-first, and fully type-safe form library for React, Solid, Vue, Svelte and more. 项目地址: https://gitcode.com/gh_mirrors/fo/formisch 点击查看 免费下载 Formisch Methods 是轻量级表单库 Formisch 提供…

2026/10/15 23:37:54 阅读更多 →
ISE 14.7与Spartan-3E:从Verilog约束到比特流的FPGA实战指南

ISE 14.7与Spartan-3E:从Verilog约束到比特流的FPGA实战指南

简介:ISE软件应用实例是一套面向FPGA初学者的实战资料包。以赛灵思Spartan-3E平台为背景,包含六个递进式项目,覆盖基本逻辑门、计数器与移位寄存器、有限状态机、数字信号处理、同步串口及存储器接口,完整演示从建立工程、编写硬件…

2026/10/15 23:37:54 阅读更多 →

最新新闻

基于planetary.js与Three.js的动态地球可视化实战

基于planetary.js与Three.js的动态地球可视化实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 1:34:40 阅读更多 →
Vue3+TypeScript全栈项目实战:工程化、类型与状态管理指南

Vue3+TypeScript全栈项目实战:工程化、类型与状态管理指南

时下很多前端同学刚接触全栈项目时,第一个念头是“我用 Vue3 写页面,后端找一个框架,接口串起来不就行了”。结果真正动手,两天就发现事情没这么简单:项目结构怎么组织、类型要不要专门建一层、接口返回不一致怎么处理…

2026/10/16 1:34:40 阅读更多 →
QQ空间说说备份完全指南:3 步把历史说说和图片搬进硬盘

QQ空间说说备份完全指南:3 步把历史说说和图片搬进硬盘

QQ空间说说备份完全指南:3 步把历史说说和图片搬进硬盘 【免费下载链接】GetQzonehistory 获取QQ空间发布的历史说说 项目地址: https://gitcode.com/GitHub_Trending/ge/GetQzonehistory GetQzonehistory 是一款免费开源的 Python 小工具。你用手机扫一次码…

2026/10/16 1:34:40 阅读更多 →
分布式定时任务系列10:XXL-job源码分析之路由策略

分布式定时任务系列10:XXL-job源码分析之路由策略

传送门 分布式定时任务系列1:XXL-job安装 分布式定时任务系列2:XXL-job使用 分布式定时任务系列3:任务执行引擎设计 分布式定时任务系列4:任务执行引擎设计续 分布式定时任务系列5:XXL-job中blockingQueue的应用 …

2026/10/16 1:34:40 阅读更多 →
Snowy 3.x 前端文件在线预览组件 XnFilePreview 使用指南:图片、PDF 与 Office 文档预览实战

Snowy 3.x 前端文件在线预览组件 XnFilePreview 使用指南:图片、PDF 与 Office 文档预览实战

后端前端认证鉴权应用安全 【免费下载链接】Snowy 💖国内首个国密前后分离快速开发平台💖《免费商用》,基于开源技术栈精心打造,融合Vue3AntDesignVue4Vite5SpringBoot3MpHuToolSa-Token。平台内置国密加解密功能,保障…

2026/10/16 1:34:40 阅读更多 →
free-account-tool兑换失败处理清单:3次失败、日上限与CDK释放机制你该知道的事

free-account-tool兑换失败处理清单:3次失败、日上限与CDK释放机制你该知道的事

【免费下载链接】free-account-tool UPI redeem only extension 项目地址: https://gitcode.com/gh_mirrors/cd/free-account-tool 点击查看 免费下载 如果你用过 free-account-tool 这款 UPI 兑换专用浏览器扩展,一定遇到过"兑换失败"的场景…

2026/10/16 1:33:40 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/15 19:41:51 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →