CSP策略中unsafe eval配置引发的安全风险与解决方案
我的写法,亲测靠谱
说实话,unsafe-eval这个东西我在实际项目中用得并不多,但每次遇到相关问题都得重新折腾一遍。之前做数据可视化平台的时候,有个动态组件加载的需求,用户可以上传自定义的JavaScript代码片段来生成图表。当时被CSP(Content Security Policy)给整得够呛,报了一堆unsafe-eval相关的错误。
我的解决思路其实很简单,就是把eval这种危险函数能不用就不用。如果真的需要动态执行代码,我会选择更安全的替代方案。比如,对于那种需要动态计算的场景,我一般这样处理:
// 不好的做法
function unsafeCalculate(expression) {
return eval(expression);
}
// 更安全的做法
function safeCalculate(expression) {
try {
// 使用Function构造器,并限制作用域
const calcFunction = new Function('params',
with(params) {
return ${expression};
}
);
return calcFunction({
Math: Math,
parseInt: parseInt,
parseFloat: parseFloat
});
} catch (error) {
console.error('Calculation error:', error);
return null;
}
}
上面这段代码的好处是限制了可访问的对象,不会让恶意代码访问到window或者其他全局对象。当然,这还不是最安全的,但如果业务确实需要动态计算,这种方式比直接eval要靠谱得多。
实际项目中的坑
上个月在做一个在线代码编辑器的时候,客户要求支持实时预览功能。这就涉及到动态执行用户输入的JavaScript代码。最开始我考虑使用eval,但CSP策略不允许unsafe-eval,浏览器直接报错了。
我折腾了半天发现,可以通过Worker线程来规避这个问题。创建一个隔离的执行环境:
class SafeExecutor {
constructor() {
this.workerCode =
self.onmessage = function(e) {
try {
const result = eval(e.data.code);
self.postMessage({ success: true, result: result });
} catch (error) {
self.postMessage({
success: false,
error: error.message
});
}
};
;
}
execute(code) {
return new Promise((resolve, reject) => {
const blob = new Blob([this.workerCode], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
worker.onmessage = (e) => {
if (e.data.success) {
resolve(e.data.result);
} else {
reject(new Error(e.data.error));
}
worker.terminate();
};
worker.postMessage({ code: code });
});
}
}
不过这里要注意一个问题,有些浏览器对Worker中使用eval也有限制。所以我后来改用了另外一种方案,就是通过创建iframe的方式来隔离执行环境。
还有一种情况是模板引擎相关的。之前用Handlebars的时候,某些复杂逻辑需要动态渲染,结果触发了unsafe-eval警告。后来我统一采用了预编译的方式,在构建阶段就把模板编译成纯JavaScript函数,避免了运行时的动态代码执行。
这几种错误写法,别再踩坑了
我见过太多错误的用法了,最典型的就是直接用eval处理JSON字符串:
// 错误示范 - 千万别这么写
const jsonString = '{"name": "test", "value": 123}';
const obj = eval('(' + jsonString + ')'); // 超级危险!
// 正确做法
const obj = JSON.parse(jsonString);
还有人喜欢用setTimeout传入字符串参数,这也可能触发unsafe-eval:
// 危险写法
const funcName = userInput;
setTimeout("console.log('Hello ' + " + funcName + ")", 1000);
// 更安全的做法
const safeFunc = () => {
console.log('Hello ' + funcName);
};
setTimeout(safeFunc, 1000);
另一个常见的坑是在字符串拼接中使用变量,特别是来自用户的输入:
// 很危险!
const userCode = document.getElementById('codeInput').value;
const finalCode = return ${userCode};
const result = new Function(finalCode)();
// 建议的做法是先进行严格的输入验证和过滤
function validateAndExecute(userCode) {
// 白名单验证,只允许特定字符
if (!/^[a-zA-Z0-9s+-*/().]+$/.test(userCode)) {
throw new Error('Invalid characters in code');
}
// 构造安全的执行环境
const restrictedScope = {
Math: Math,
Number: Number,
String: String
};
const func = new Function(...Object.keys(restrictedScope), return ${userCode});
return func(...Object.values(restrictedScope));
}
这些写法看起来没什么问题,但一旦用户输入恶意代码,后果不堪设想。我之前就因为这个问题差点被老板骂死。
CSP配置的那些事儿
说到CSP配置,我也踩了不少坑。最开始我直接在meta标签里加上unsafe-eval,觉得反正解决了问题就行:
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-eval';">
但实际上这样做的安全风险很大。后来我改用更精确的策略,只对必要的页面开启unsafe-eval:
// 服务器端动态设置CSP头
app.use('/admin', (req, res, next) => {
res.setHeader('Content-Security-Policy', "script-src 'self' 'unsafe-eval';");
next();
});
// 其他路由使用严格策略
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy', "script-src 'self';");
next();
});
这样做的好处是只有管理员界面才允许eval,普通用户的页面仍然是安全的。不过这种方式也有缺点,就是管理起来比较麻烦,需要仔细控制哪些页面真正需要unsafe-eval。
性能考虑和调试技巧
还有一点需要注意,即使你不得不使用unsafe-eval,也要考虑到性能问题。eval和new Function都会阻止JavaScript引擎的优化,导致执行效率下降。我一般会在生产环境中记录相关的性能指标:
function measureExecutionTime(fn, ...args) {
const startTime = performance.now();
const result = fn.apply(null, args);
const endTime = performance.now();
console.log(Execution time: ${endTime - startTime}ms);
return result;
}
调试的时候我发现,Chrome DevTools的Performance面板可以清楚地看到eval调用的影响。如果你看到大片的黄色标记,基本就能确定是eval在搞鬼了。
最后一个小技巧,就是可以用try-catch包装所有的动态执行代码,并且记录详细的错误信息:
function safeDynamicExecute(code) {
try {
// 添加时间限制,防止无限循环
const timeoutId = setTimeout(() => {
throw new Error('Execution timeout');
}, 5000);
const result = eval(code);
clearTimeout(timeoutId);
return result;
} catch (error) {
console.error('Dynamic execution failed:', error);
// 记录错误日志,便于后续分析
logError(error, code);
return null;
}
}
以上是我踩坑后的总结,希望对你有帮助。这个话题的拓展用法还有很多,后续会继续分享这类博客。如果有更优的实现方式欢迎评论区交流。

暂无评论