Vue3 Composition API无eval实现模板动态替换ref值的安全方案
问题说明
- 开发支持用户自定义文档模板的系统,设计了动态数据注入语法,占位符格式为
#属性名 from ref变量名#,示例:#firstName from client#,其中client是通过ref()定义的响应式变量,firstName是该响应式对象的属性。 - 需求是实现占位符到实际数据的替换,因用户输入不可信,已配置
allowedTemplateTags白名单数组,仅解析正则匹配到的#..#格式内容,非白名单片段不做解析。 - 当前实现基于
eval+白名单,需要确认该方案是否存在安全隐患,以及是否有更优的基于ref值的动态替换方案。
现有实现代码如下:
function processTemplate(text: string): string { const re = new RegExp(/#\w+ from \w+#/, 'g') const matches = text.match(re) ?? [] for (const match of matches) { if (!allowedTemplateTags.includes(match)) { continue } const split = match.replace(/#/g, '').split(' from ') const replaceRegEx = new RegExp(`(${match})`, 'g') // eslint-disable-next-line @typescript-eslint/no-unsafe-argument text = text.replace(replaceRegEx, eval(`${split[1]}?.value.${split[0]}`)) } return text }
现有方案的安全隐患
- 核心风险来自
eval的动态执行特性:即使配置了白名单,一旦出现正则绕过(比如特殊Unicode字符绕过\w匹配规则、白名单配置失误),攻击者可以构造恶意占位符执行上下文中的任意代码,触发XSS、敏感数据泄露等问题。 - 性能表现差:
eval无法被JS引擎做JIT优化,同时现有逻辑每次匹配都要重新构造正则、多次遍历替换文本,大模板场景下执行效率很低。 - 可维护性差:TypeScript无法对eval内的动态代码做静态类型校验,很容易出现隐式类型错误、属性名拼写错误无法提前发现的问题。
无eval的安全实现方案
核心思路是完全隔离可访问的变量范围,把所有允许模板访问的ref显式注册到独立的上下文对象中,通过键值匹配取值,全程不做动态代码执行:
// 1. 显式注册所有允许模板访问的ref,不要自动收集作用域内的全部变量,从根源缩小攻击面 const templateSafeContext: Record<string, { value: Record<string, string | number | boolean> }> = { client: client, // 示例:注册client这个ref // 其他需要暴露给模板的ref都在这里手动添加 } function processTemplate(text: string): string { const placeholderRe = /#(\w+) from (\w+)#/g // 白名单转Set提升查询性能 const allowedTags = new Set(allowedTemplateTags) // 单次正则遍历完成全部替换,不需要多次循环 return text.replace(placeholderRe, (match, propKey, refKey) => { // 第一道校验:白名单过滤 if (!allowedTags.has(match)) return match // 第二道校验:从安全上下文取ref,不存在就原样返回 const targetRef = templateSafeContext[refKey] if (!targetRef?.value) return match // 取对应属性值 const propValue = targetRef.value[propKey] if (!['string', 'number', 'boolean'].includes(typeof propValue)) return '' // 第三道防护:XSS转义,避免输出内容带恶意脚本 return String(propValue).replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char])) }) }
方案优势
- 零eval无代码执行风险:所有可访问的变量必须手动注册到安全上下文,攻击者无法通过构造占位符访问全局对象、未注册的内部变量。
- 性能更高:单次正则遍历完成替换,没有动态代码执行开销,大文本场景下替换速度比原方案高一个数量级。
- 类型安全:可以给
templateSafeContext配置明确的TypeScript类型,编译阶段就能发现ref名、属性名拼写错误的问题。 - 内置多层防护:白名单校验、上下文隔离、输出转义三层防护,即使某一层配置失误也不会出现严重安全问题。
扩展注意事项
如果后续需要支持嵌套属性取值(比如#address.city from client#),不要用eval解析属性路径,可以写一个独立的安全取值函数,按.拆分路径后逐层从对象上取值,全程避免动态代码执行。
内容的提问来源于stack exchange,提问作者Trent
相关产品推荐
相关产品推荐

